diff --git a/.azure-pipelines/publish-docker.yml b/.azure-pipelines/publish-docker.yml index 53095d1b4..875a2e103 100644 --- a/.azure-pipelines/publish-docker.yml +++ b/.azure-pipelines/publish-docker.yml @@ -69,8 +69,8 @@ extends: echo '{ "data-root": "/mnt/docker" }' | sudo tee /etc/docker/daemon.json sudo service docker start - # Used by scripts/download_driver.sh on the host. The repository root is the - # Docker build context, so .dockerignore keeps this file out of the images. + # Used by scripts/download_driver.sh on the host. The Docker build context is + # utils/docker (see build.sh), so this file never reaches the images. - task: Bash@3 displayName: "setup .npmrc" inputs: @@ -115,9 +115,9 @@ extends: displayName: "Build & publish Docker images" inputs: targetType: "inline" - # The authenticated settings.xml is passed to `docker build` as a secret so - # that Maven inside the image also resolves from DevDiv_PublicPackages. - script: MAVEN_SETTINGS_SECRET="$HOME/.m2/settings.xml" ./utils/docker/publish_docker.sh ${{ parameters.releaseChannel }} + # build.sh builds the client on the host, where ~/.m2/settings.xml mirrors Maven + # Central to DevDiv_PublicPackages. + script: ./utils/docker/publish_docker.sh ${{ parameters.releaseChannel }} env: ACR_CACHE_PREFIX: "playwright.azurecr.io/cached/" UBUNTU_MIRROR_PREFIX: "azure." diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 7dc6ab376..000000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -# The repository root is the Docker build context (see utils/docker/build.sh), -# and the image build copies all of it. Keep VCS metadata, registry credentials -# and local environment files out of the build context. -**/.git -**/.npmrc -**/.env* -# Downloaded by publish_docker.sh after the first push. -utils/docker/oras diff --git a/utils/docker/.dockerignore b/utils/docker/.dockerignore new file mode 100644 index 000000000..94c85dfae --- /dev/null +++ b/utils/docker/.dockerignore @@ -0,0 +1,11 @@ +# This directory is the Docker build context (see build.sh). The image reads only the +# artifacts staged in dist/ and the Maven tarball in maven/; keep everything else, such +# as the oras download of publish_docker.sh, out of the build context. +* +!dist +!maven +# Should the build context ever grow, keep VCS metadata, registry credentials and local +# environment files out of it regardless. +**/.git +**/.npmrc +**/.env* diff --git a/utils/docker/.gitignore b/utils/docker/.gitignore index f5ca1e18c..ba5e6d750 100644 --- a/utils/docker/.gitignore +++ b/utils/docker/.gitignore @@ -1,2 +1,2 @@ oras/ - +dist/ diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 316a95d4f..ea8085b43 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -31,7 +31,7 @@ RUN find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ # Java 25, so we install latest Maven from Apache instead. build.sh # downloads it into utils/docker/maven. ARG MAVEN_VERSION -RUN --mount=type=bind,source=utils/docker/maven,target=/tmp/maven \ +RUN --mount=type=bind,source=maven,target=/tmp/maven \ tar zxf /tmp/maven/apache-maven-$MAVEN_VERSION-bin.tar.gz -C /opt/ && \ ln -s /opt/apache-maven-$MAVEN_VERSION/bin/mvn /usr/local/bin/ @@ -50,25 +50,16 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # 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 +# The playwright-java artifacts built by build.sh go into the local Maven repository so +# that Maven projects built in the image resolve them without downloading. +COPY dist/m2 /root/.m2/repository -COPY . /tmp/pw-java - -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). -RUN --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - 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 \ - -D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \ - 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 && \ +# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. +RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + mkdir /ms-playwright && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ # Workaround for https://github.com/microsoft/playwright/issues/27313 # While the gstreamer plugin load process can be in-process, it ended up throwing # an error that it can't have libsoup2 and libsoup3 in the same process because diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 5ccd4d7bf..4b2c480fe 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -31,7 +31,7 @@ RUN find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ # Java 25, so we install latest Maven from Apache instead. build.sh # downloads it into utils/docker/maven. ARG MAVEN_VERSION -RUN --mount=type=bind,source=utils/docker/maven,target=/tmp/maven \ +RUN --mount=type=bind,source=maven,target=/tmp/maven \ tar zxf /tmp/maven/apache-maven-$MAVEN_VERSION-bin.tar.gz -C /opt/ && \ ln -s /opt/apache-maven-$MAVEN_VERSION/bin/mvn /usr/local/bin/ @@ -50,24 +50,15 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # 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 +# The playwright-java artifacts built by build.sh go into the local Maven repository so +# that Maven projects built in the image resolve them without downloading. +COPY dist/m2 /root/.m2/repository -COPY . /tmp/pw-java - -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). -RUN --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - 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 \ - -D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \ - 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 && \ +# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. +RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + mkdir /ms-playwright && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ 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 7dabf1f96..e89578635 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -44,24 +44,15 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # 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 - -COPY . /tmp/pw-java - -# The optional Maven settings secret routes dependency resolution through a mirror -# (see .azure-pipelines/publish-docker.yml). -RUN --mount=type=secret,id=mavensettings,target=/root/.m2/settings.xml,required=false \ - 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 \ - -D exec.args="install" -f playwright/pom.xml --no-transfer-progress && \ - 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 && \ +# The playwright-java artifacts built by build.sh go into the local Maven repository so +# that Maven projects built in the image resolve them without downloading. +COPY dist/m2 /root/.m2/repository + +# The CLI runs from the jars staged by build.sh; the mount leaves nothing in the image. +RUN --mount=type=bind,source=dist/cli,target=/tmp/pw-java \ + mkdir /ms-playwright && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install-driver && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI install --with-deps && \ + java -cp '/tmp/pw-java/*' com.microsoft.playwright.CLI mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/build.sh b/utils/docker/build.sh index c1225245d..c951d9354 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -10,13 +10,14 @@ if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then echo "" echo " docker run --rm -it playwright:localbuild-noble /bin/bash" echo "" - echo "NOTE: this requires on Playwright PIP dependencies to be installed" + echo "NOTE: this requires a JDK, Maven and npm on the host: the client is built here" + echo "and the image only installs the result." echo "" exit 0 fi function cleanup() { - : + rm -rf dist } trap "cleanup; cd $(pwd -P)" EXIT @@ -34,21 +35,42 @@ fi PW_TARGET_ARCH=$(echo $1 | cut -c3-) -# Assemble the driver on the host where npm is available; the Dockerfile picks -# it up via `COPY . /tmp/pw-java`. -../../scripts/download_driver.sh +# The host Maven settings (~/.m2/settings.xml) route dependency resolution through the +# mirror of the release pipeline, which cannot reach Maven Central directly. +MVN=(mvn --no-transfer-progress) -# The jammy and noble images install this Maven (see the Dockerfiles). Fetch it with the -# host's Maven so that it resolves through the configured mirror: the release pipeline -# cannot reach Maven Central directly (see .azure-pipelines/settings.xml). -MAVEN_VERSION=3.9.12 -mvn dependency:copy -Dartifact="org.apache.maven:apache-maven:${MAVEN_VERSION}:tar.gz:bin" \ - -DoutputDirectory=maven --no-transfer-progress -q +# Like the other language ports, build the client on the host, where npm, a JDK and +# Maven are available, and stage what the Dockerfile copies into the image under dist/. +# The build context is this directory, so nothing else reaches the image. +../../scripts/download_driver.sh +"${MVN[@]}" -B install -D skipTests -f ../../pom.xml -SECRET_ARGS=() -if [[ -n "${MAVEN_SETTINGS_SECRET:-}" ]]; then - SECRET_ARGS+=(--secret "id=mavensettings,src=${MAVEN_SETTINGS_SECRET}") +if ! PW_INFO=$("${MVN[@]}" -q -N -f ../../pom.xml exec:exec -D exec.executable=echo \ + -D exec.args='${project.version} ${settings.localRepository}'); then + # With -q Maven prints its errors to stdout, which was captured above. + echo "ERROR: failed to read the project version and the local repository: ${PW_INFO}" + exit 1 fi +read -r PW_VERSION LOCAL_REPO <<< "$PW_INFO" +rm -rf dist + +# dist/m2: the playwright-java artifacts of this build, for the local Maven repository of +# the image. Projects built in the image then resolve them without downloading. +for MODULE in parent-pom driver driver-bundle playwright; do + mkdir -p "dist/m2/com/microsoft/playwright/$MODULE" + cp -R "$LOCAL_REPO/com/microsoft/playwright/$MODULE/$PW_VERSION" "dist/m2/com/microsoft/playwright/$MODULE/" +done + +# dist/cli: the client jar and its runtime dependencies, which the Dockerfile runs the +# CLI from to install the driver, the browsers and their dependencies. +"${MVN[@]}" -q -f ../../playwright/pom.xml dependency:copy-dependencies \ + -D includeScope=runtime -D outputDirectory="$PWD/dist/cli" +cp "../../playwright/target/playwright-$PW_VERSION.jar" dist/cli/ + +# The jammy and noble images install this Maven (see the Dockerfiles). +MAVEN_VERSION=3.9.12 +"${MVN[@]}" -q dependency:copy -D artifact="org.apache.maven:apache-maven:${MAVEN_VERSION}:tar.gz:bin" \ + -D outputDirectory=maven # Keep each arch image a plain single-platform manifest without the unknown/unknown platform entry. export BUILDX_NO_DEFAULT_ATTESTATIONS=1 @@ -69,8 +91,7 @@ for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)); do --build-arg "MAVEN_VERSION=${MAVEN_VERSION}" \ --build-arg ACR_CACHE_PREFIX="${ACR_CACHE_PREFIX}" \ --build-arg UBUNTU_MIRROR_PREFIX="${UBUNTU_MIRROR_PREFIX}" \ - "${SECRET_ARGS[@]}" \ - -t "$3" -f "Dockerfile.$2" ../../; then + -t "$3" -f "Dockerfile.$2" .; then exit 0 fi if (( attempt < MAX_ATTEMPTS )); then