diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67d527b..2a3a214 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,5 +55,5 @@ jobs: - name: Build with Maven run: mvn -B clean install -Dno-format --no-transfer-progress - #- name: Build with Maven (Native) - # run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip --no-transfer-progress \ No newline at end of file + - name: Build with Maven (Native) + run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip --no-transfer-progress \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java b/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java index 30cff28..c7d8a36 100644 --- a/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/playwright/deployment/PlaywrightProcessor.java @@ -1,7 +1,36 @@ package io.quarkiverse.playwright.deployment; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.impl.driver.jar.DriverJar; +import com.microsoft.playwright.options.HttpHeader; +import com.microsoft.playwright.options.Timing; +import com.microsoft.playwright.options.ViewportSize; + +import io.quarkiverse.playwright.runtime.PlaywrightRecorder; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.NativeImageEnableAllCharsetsBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.logging.Log; class PlaywrightProcessor { @@ -11,4 +40,133 @@ class PlaywrightProcessor { FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } + + @BuildStep + void indexTransitiveDependencies(BuildProducer index) { + index.produce(new IndexDependencyBuildItem("com.microsoft.playwright", "playwright")); + index.produce(new IndexDependencyBuildItem("com.microsoft.playwright", "driver-bundle")); + index.produce(new IndexDependencyBuildItem("com.microsoft.playwright", "driver")); + } + + @BuildStep + NativeImageEnableAllCharsetsBuildItem enableAllCharsetsBuildItem() { + return new NativeImageEnableAllCharsetsBuildItem(); + } + + @BuildStep + void registerForReflection(CombinedIndexBuildItem combinedIndex, BuildProducer reflectiveClass) { + //@formatter:off + final List classNames = new ArrayList<>(); + + classNames.add("com.microsoft.playwright.impl.Message"); + classNames.add("com.microsoft.playwright.impl.SerializedArgument"); + classNames.add("com.microsoft.playwright.impl.SerializedValue"); + classNames.add("com.microsoft.playwright.impl.SerializedValue$O"); + classNames.add(Browser.CloseOptions.class.getName()); + classNames.add(Browser.NewContextOptions.class.getName()); + classNames.add(Browser.NewPageOptions.class.getName()); + classNames.add(Browser.StartTracingOptions.class.getName()); + classNames.add(DriverJar.class.getName()); + classNames.add(ElementHandle.CheckOptions.class.getName()); + classNames.add(ElementHandle.ClickOptions.class.getName()); + classNames.add(ElementHandle.DblclickOptions.class.getName()); + classNames.add(ElementHandle.FillOptions.class.getName()); + classNames.add(ElementHandle.HoverOptions.class.getName()); + classNames.add(ElementHandle.InputValueOptions.class.getName()); + classNames.add(ElementHandle.PressOptions.class.getName()); + classNames.add(ElementHandle.ScreenshotOptions.class.getName()); + classNames.add(ElementHandle.ScrollIntoViewIfNeededOptions.class.getName()); + classNames.add(ElementHandle.SelectTextOptions.class.getName()); + classNames.add(ElementHandle.SetInputFilesOptions.class.getName()); + classNames.add(ElementHandle.TapOptions.class.getName()); + classNames.add(ElementHandle.TypeOptions.class.getName()); + classNames.add(ElementHandle.UncheckOptions.class.getName()); + classNames.add(ElementHandle.WaitForElementStateOptions.class.getName()); + classNames.add(ElementHandle.WaitForSelectorOptions.class.getName()); + classNames.add(HttpHeader.class.getName()); + classNames.add(Timing.class.getName()); + classNames.add(ViewportSize.class.getName()); + classNames.addAll(collectImplementors(combinedIndex, Playwright.class.getName())); + + //@formatter:on + final TreeSet uniqueClasses = new TreeSet<>(classNames); + Log.debugf("Playwright Reflection: %s", uniqueClasses); + + reflectiveClass.produce( + ReflectiveClassBuildItem.builder(uniqueClasses.toArray(new String[0])).constructors().methods().fields() + .serialization().unsafeAllocated().build()); + } + + @BuildStep(onlyIf = IsNormal.class) + @Record(ExecutionTime.RUNTIME_INIT) + void bindDrivers(PlaywrightRecorder recorder) { + recorder.initialize(); + } + + @BuildStep + void registerDrivers(BuildProducer nativeImageResourcePatterns) { + final NativeImageResourcePatternsBuildItem.Builder builder = NativeImageResourcePatternsBuildItem.builder(); + builder.includeGlob("driver/**"); + nativeImageResourcePatterns.produce(builder.build()); + } + + protected List collectClassesInPackage(CombinedIndexBuildItem combinedIndex, String packageName) { + final List classes = new ArrayList<>(); + final List packages = new ArrayList<>(combinedIndex.getIndex().getSubpackages(packageName)); + packages.add(DotName.createSimple(packageName)); + for (DotName aPackage : packages) { + final List packageClasses = combinedIndex.getIndex() + .getClassesInPackage(aPackage) + .stream() + .map(ClassInfo::toString) + .toList(); + classes.addAll(packageClasses); + } + Log.debugf("Package Classes: %s", classes); + return classes; + } + + protected List collectInterfacesInPackage(CombinedIndexBuildItem combinedIndex, String packageName) { + final List classes = new ArrayList<>(); + final List packages = new ArrayList<>(combinedIndex.getIndex().getSubpackages(packageName)); + packages.add(DotName.createSimple(packageName)); + for (DotName aPackage : packages) { + final List packageClasses = combinedIndex.getIndex() + .getClassesInPackage(aPackage) + .stream() + .filter(ClassInfo::isInterface) // Filter only interfaces + .map(ClassInfo::toString) + .toList(); + classes.addAll(packageClasses); + } + Log.debugf("Package Interfaces: %s", classes); + return classes; + } + + protected List collectSubclasses(CombinedIndexBuildItem combinedIndex, String className) { + List classes = combinedIndex.getIndex() + .getAllKnownSubclasses(DotName.createSimple(className)) + .stream() + .map(ClassInfo::toString) + .collect(Collectors.toList()); + classes.add(className); + Log.debugf("Subclasses: %s", classes); + return classes; + } + + protected List collectImplementors(CombinedIndexBuildItem combinedIndex, String className) { + Set classes = combinedIndex.getIndex() + .getAllKnownImplementors(DotName.createSimple(className)) + .stream() + .map(ClassInfo::toString) + .collect(Collectors.toCollection(HashSet::new)); + classes.add(className); + Set subclasses = new HashSet<>(); + for (String implementationClass : classes) { + subclasses.addAll(collectSubclasses(combinedIndex, implementationClass)); + } + classes.addAll(subclasses); + Log.debugf("Implementors: %s", classes); + return new ArrayList<>(classes); + } } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 9a60037..a06f3fd 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -51,9 +51,21 @@ rest-assured test - + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + docker + ${project.basedir} + + + + io.quarkus @@ -122,5 +134,49 @@ native + + native-docker + + + native-docker + + + + true + true + + --trace-object-instantiation=java.awt.BasicStroke + + + + clean package + + + org.codehaus.mojo + exec-maven-plugin + + + + docker-build + package + + exec + + + + build + -f + src/main/docker/Dockerfile.native + -t + playwright/integration-test:${project.version} + . + + + + + + + + \ No newline at end of file diff --git a/integration-tests/src/main/docker/Dockerfile.native b/integration-tests/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..f5516bf --- /dev/null +++ b/integration-tests/src/main/docker/Dockerfile.native @@ -0,0 +1,36 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t playwright/integration-test . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 playwright/integration-test +# +### +FROM mcr.microsoft.com/playwright:v1.48.1-noble + +WORKDIR /work/ + +# Set permissions for /work directory and grant full access to /tmp +RUN chown 1001:root /work \ + && chmod g+rwX /work \ + && chown 1001:root /work + +# Copy necessary files and adjust permissions +COPY --chown=1001:root target/*.properties target/*.so /work/ +COPY --chown=1001:root target/*-runner /work/application + +# Make application executable for all users +RUN chmod ugo+x /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java b/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java index b6264eb..f7ee45a 100644 --- a/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java +++ b/integration-tests/src/main/java/io/quarkiverse/playwright/it/PlaywrightResource.java @@ -24,6 +24,8 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import org.jboss.logging.Logger; + import com.microsoft.playwright.Browser; import com.microsoft.playwright.BrowserType; import com.microsoft.playwright.Page; @@ -32,24 +34,27 @@ @Path("/playwright") @ApplicationScoped public class PlaywrightResource { - // add some rest methods here + + private static final Logger log = Logger.getLogger(PlaywrightResource.class); @GET public String google() { - String pageTitle = "Hello playwright"; + String pageTitle; final BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions() .setHeadless(true) .setChromiumSandbox(false) .setChannel("") .setArgs(List.of("--disable-gpu")); final Map env = new HashMap<>(System.getenv()); + env.put("DEBUG", "pw:api"); try (Playwright playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env))) { try (Browser browser = playwright.chromium().launch(launchOptions)) { Page page = browser.newPage(); page.navigate("https://www.google.com/"); pageTitle = page.title(); + log.infof("Page title: %s", pageTitle); } } return pageTitle; } -} \ No newline at end of file +} diff --git a/runtime/src/main/java/io/quarkiverse/playwright/runtime/PlaywrightRecorder.java b/runtime/src/main/java/io/quarkiverse/playwright/runtime/PlaywrightRecorder.java new file mode 100644 index 0000000..c6387b3 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/playwright/runtime/PlaywrightRecorder.java @@ -0,0 +1,35 @@ +package io.quarkiverse.playwright.runtime; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.util.Collections; + +import org.jboss.logging.Logger; + +import com.microsoft.playwright.impl.driver.jar.DriverJar; + +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class PlaywrightRecorder { + + private static final Logger log = Logger.getLogger(PlaywrightRecorder.class); + + public void initialize() { + try { + URI uri = DriverJar.getDriverResourceURI(); + log.infof("Playwright Driver: %s", uri); + FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap()); + if (fs == null) { + log.errorf("FileSystem Error NULL: %s", uri); + } + DriverJar jar = new DriverJar(); + log.debugf("Playwright Driver Directory: %s", jar.driverDir()); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + } +}