diff --git a/.azure-pipelines/publish-docker.yml b/.azure-pipelines/publish-docker.yml index 53095d1b4..1eece8256 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,7 @@ 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 }} + 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/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index cd8f86606..b6f601177 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -57,13 +57,11 @@ jobs: - name: Copy repository inside docker container run: | docker cp . "$CONTAINER_ID":/home/pwuser/playwright - # /root/.m2 was populated as root during image build; move it to - # pwuser so the locally-installed SNAPSHOT artifacts resolve. - docker exec --user root "$CONTAINER_ID" bash -c ' - chown -R pwuser /home/pwuser/playwright - mv /root/.m2 /home/pwuser/.m2 - chown -R pwuser /home/pwuser/.m2 - ' + # build.sh installed the SNAPSHOT artifacts into the local Maven repository + # of the runner; give them to pwuser so the test projects resolve them. + docker exec "$CONTAINER_ID" mkdir -p /home/pwuser/.m2/repository/com/microsoft + docker cp ~/.m2/repository/com/microsoft/playwright "$CONTAINER_ID":/home/pwuser/.m2/repository/com/microsoft/playwright + docker exec --user root "$CONTAINER_ID" chown -R pwuser /home/pwuser/playwright /home/pwuser/.m2 - name: Run smoke tests in container run: | diff --git a/utils/docker/.dockerignore b/utils/docker/.dockerignore new file mode 100644 index 000000000..c30d25e03 --- /dev/null +++ b/utils/docker/.dockerignore @@ -0,0 +1,11 @@ +# This directory is the Docker build context (see build.sh). The image build reads only +# the client jars 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..fce58fc82 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,13 @@ 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 client jar and its runtime dependencies, staged by build.sh, are only mounted to +# run the CLI; they do not go into the image. +RUN --mount=type=bind,source=dist,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..07d4b8c3b 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,12 @@ 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 client jar and its runtime dependencies, staged by build.sh, are only mounted to +# run the CLI; they do not go into the image. +RUN --mount=type=bind,source=dist,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..58b0ff6ac 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -44,24 +44,12 @@ 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 client jar and its runtime dependencies, staged by build.sh, are only mounted to +# run the CLI; they do not go into the image. +RUN --mount=type=bind,source=dist,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..2494eade8 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 runs it to install the browsers." echo "" exit 0 fi function cleanup() { - : + rm -rf dist } trap "cleanup; cd $(pwd -P)" EXIT @@ -34,21 +35,32 @@ 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 +# In the release pipeline ~/.m2/settings.xml routes all Maven downloads through the +# DevDiv_PublicPackages feed instead of Maven Central, which the agent cannot reach. +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 the client jar and its runtime dependencies under +# dist/. The Dockerfile runs the CLI from there to install the browsers and their +# dependencies. 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_VERSION=$("${MVN[@]}" -q -N -f ../../pom.xml exec:exec -D exec.executable=echo \ + -D exec.args='${project.version}'); then + # With -q Maven prints its errors to stdout, which was captured above. + echo "ERROR: failed to read the project version: ${PW_VERSION}" + exit 1 fi +rm -rf dist +"${MVN[@]}" -q -f ../../playwright/pom.xml dependency:copy-dependencies \ + -D includeScope=runtime -D outputDirectory="$PWD/dist" +cp "../../playwright/target/playwright-$PW_VERSION.jar" dist/ + +# 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 +81,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