diff --git a/.github/workflows/extended-native-image.yml b/.github/workflows/extended-native-image.yml new file mode 100644 index 00000000000..5146be856fc --- /dev/null +++ b/.github/workflows/extended-native-image.yml @@ -0,0 +1,292 @@ +# The native-image gate for the extended branch: an AWT-free Compose application packaged as +# one executable on macOS arm64 and Linux x64, then started. There is no paths filter, because +# this may become a required check and a required check that is skipped blocks the merge. +# +# The window modules, skiko's static archive and the plugin are all built here from pinned +# sources. Bump CORE_EXTENDED_REF to take newer window code. +name: Extended native image + +on: + pull_request: + branches: [ extended ] + workflow_dispatch: + +concurrency: + group: extended-native-image-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # thisisthepy/compose-multiplatform-core-extended, the commit the window modules and + # extended/skiko come from. + CORE_EXTENDED_REF: 87f67ef9a3c5eff81da9f0999a71addfa6ac7295 + PLUGIN_VERSION: 1.11.1-extended-dev + COMPOSE_VERSION: 1.11.1 + NIK_BASE: https://github.com/bell-sw/LibericaNIK/releases/download/25.0.4.1+1-25.0.4.1+2 + +jobs: + # compose-rust and pythonx constraints, kept apart from the standalone samples: the upcall + # table and the absence of runtime reflection matter to them, not to an ordinary Kotlin app. + config-check: + name: "compose-rust/pythonx: window upcall table and no reflection" + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + extended/native-image + .github + - uses: actions/checkout@v4 + with: + repository: thisisthepy/compose-multiplatform-core-extended + ref: ${{ env.CORE_EXTENDED_REF }} + path: core-extended + sparse-checkout: extended/window + - name: No runtime reflection, upcalls through the table + run: | + meta=extended/native-image/hello-awtfree/src/main/native-image/reachability-metadata.json + if [ -f "$meta" ]; then + bash extended/native-image/check-native-config.sh core-extended --strict "$meta" + else + echo "hello-awtfree does not exist yet: checking the window sources only" + bash extended/native-image/check-native-config.sh core-extended + fi + + plugin-dsl: + name: Plugin DSL additive + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + filter: blob:none + - name: Upstream DSL gains declarations and loses none + run: bash extended/native-image/check-plugin-dsl-additive.sh origin/jb-main + + # The plain JVM way to ship, built and started standalone. Independent of the native-image + # cells below, so a JVM regression is not hidden behind a native-image one. + jvm: + name: JVM hello (${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + include: + - { name: macos-arm64, os: macos-26 } + - { name: linux-x64, os: ubuntu-24.04 } + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + - name: Linux packages + if: runner.os == 'Linux' + run: | + sudo apt-get update -q + sudo apt-get install -y -q --no-install-recommends xvfb xauth libgl1-mesa-dri libxtst6 libxrender1 libxi6 fonts-noto-cjk binutils fakeroot + - name: Plugin to mavenLocal + working-directory: gradle-plugins + run: ./gradlew --no-daemon -q :compose:publishToMavenLocal :jdk-version-probe:publishToMavenLocal -Pdeploy.version=${{ env.PLUGIN_VERSION }} -Pcompose.version=${{ env.COMPOSE_VERSION }} + - name: createDistributable + working-directory: extended/samples/hello-jvm + run: ./gradlew --no-daemon createDistributable + - name: Start the packaged app + working-directory: extended/samples/hello-jvm + shell: bash + run: | + set -euo pipefail + launcher=$(find build/compose/binaries -type f -path '*bin/hello-jvm' -o -type f -path '*MacOS/hello-jvm' | head -1) + echo "launcher: $launcher" + png="$RUNNER_TEMP/jvm-self-check.png" + if [ "$RUNNER_OS" = Linux ]; then + HELLO_SELF_CHECK="$png" xvfb-run -a timeout 120 "$launcher" + else + HELLO_SELF_CHECK="$png" perl -e 'alarm 120; exec @ARGV' "$launcher" + fi + test -s "$png" + + package: + name: Package (${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + include: + - name: macos-arm64 + os: macos-26 + nik: macos-aarch64 + - name: linux-x64 + os: ubuntu-24.04 + nik: linux-amd64 + # Kotlin/Native packaging cells go here. Each one runs check-native-config.sh (already + # scans extended/window/native) and, after linking, `nm` on the executable for + # Objective-C or class-name lookups; see the K/N section of the plugin change list. + # Kotlin/Native packaging cells go here. The K/N window agent adds them: one per + # platform, building the native window modules into an executable and starting it. + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + env: + SAMPLE_AWTFREE: extended/native-image/hello-awtfree + steps: + - uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + with: + repository: thisisthepy/compose-multiplatform-core-extended + ref: ${{ env.CORE_EXTENDED_REF }} + path: core-extended + sparse-checkout: | + extended/window + extended/skiko + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Linux packages + if: runner.os == 'Linux' + run: | + sudo apt-get update -q + sudo apt-get install -y -q --no-install-recommends \ + build-essential pkg-config xvfb xauth libgl1-mesa-dev libgl1-mesa-dri \ + libx11-dev libxext-dev libxi-dev libxrender-dev libxtst-dev libxrandr-dev libxcursor-dev \ + libfontconfig1-dev libfreetype-dev libdbus-1-dev zlib1g-dev fonts-noto-cjk jq + + - name: Liberica NIK 25 Full + shell: bash + run: | + curl -fsSL -o "$RUNNER_TEMP/nik.tar.gz" "$NIK_BASE/bellsoft-liberica-vm-full-openjdk25.0.4.1+2-25.0.4.1+1-${{ matrix.nik }}.tar.gz" + mkdir -p "$RUNNER_TEMP/nik" && tar xzf "$RUNNER_TEMP/nik.tar.gz" -C "$RUNNER_TEMP/nik" + rel="$(find "$RUNNER_TEMP/nik" -name release -type f | awk '{print length, $0}' | sort -n | head -1 | cut -d' ' -f2-)" + home="$(dirname "$rel")" + echo "GRAALVM_HOME=$home" >> "$GITHUB_ENV" + "$home/bin/native-image" --version + + - name: Does the AwtFree sample exist + id: sample + shell: bash + run: | + if [ -f "$SAMPLE_AWTFREE/build.gradle.kts" ]; then echo "awtfree=true" >> "$GITHUB_OUTPUT"; else echo "awtfree=false" >> "$GITHUB_OUTPUT"; fi + + # skiko without JAWT, for the AWT-free sample. Keyed on the pinned commit, which pins the + # script and skiko's revision with it. + - name: skiko static archive cache (no JAWT) + id: skiko-cache + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/skiko-static-nojawt + key: skiko-static-nojawt-${{ matrix.name }}-${{ env.CORE_EXTENDED_REF }} + + - name: Build skiko static archive (no JAWT) + if: steps.skiko-cache.outputs.cache-hit != 'true' + shell: bash + run: bash core-extended/extended/skiko/build-skiko-static-jvm.sh --no-jawt "$RUNNER_TEMP/skiko-static-nojawt" + + # core-extended's window modules to mavenLocal, the way its extended-window-publish + # workflow does it: Amper rejects two modules both named macos in one project, so each + # publication is built from a project of the modules it needs. + - name: Publish the window modules to mavenLocal + shell: bash + working-directory: core-extended/extended/window + run: | + set -euo pipefail + # The module lists are core-extended's own extended-window-publish matrix. + if [ "$RUNNER_OS" = macOS ]; then + modules="common native/macos graalvm/graalvm-macos graalvm/graalvm-linux" + else + modules="common graalvm/graalvm-macos graalvm/graalvm-linux" + fi + cp project.yaml project.yaml.all + printf 'modules:\n' > project.yaml + for m in $modules; do printf ' - %s\n' "$m" >> project.yaml; done + ./kotlin publish mavenLocal + cp project.yaml.all project.yaml + find ~/.m2/repository/org/thisisthepy/compose/window -name '*.pom' | sort + + - name: Plugin to mavenLocal + working-directory: gradle-plugins + run: ./gradlew --no-daemon -q :compose:publishToMavenLocal :jdk-version-probe:publishToMavenLocal -Pdeploy.version=${{ env.PLUGIN_VERSION }} -Pcompose.version=${{ env.COMPOSE_VERSION }} + + # The AwtFree sample: packageNativeImage with the no-JAWT archive, then a headless start. + - name: packageNativeImage (AwtFree sample) + if: steps.sample.outputs.awtfree == 'true' + working-directory: ${{ env.SAMPLE_AWTFREE }} + env: + SKIKO_STATIC: ${{ runner.temp }}/skiko-static-nojawt/out/${{ matrix.name }} + shell: bash + run: ./gradlew --no-daemon --info packageNativeImage + + # The window layer link check: a plain AwtFree main that opens one window through the + # extended window module. packageNativeImage compiles and links appkit_window.m or + # x11_window.c and the upcall table, so a link error in the window sources shows here. + - name: packageNativeImage (AwtFree link sample) + working-directory: extended/native-image/hello-awtfree-link + env: + SKIKO_STATIC: ${{ runner.temp }}/skiko-static-nojawt/out/${{ matrix.name }} + CORE_EXTENDED: ${{ github.workspace }}/core-extended + shell: bash + run: ./gradlew --no-daemon --info packageNativeImage + + - name: Start the AwtFree link sample + working-directory: extended/native-image/hello-awtfree-link + shell: bash + run: | + set -euo pipefail + exe=$(find build/compose/native-image -maxdepth 2 -type f -perm -u+x | head -1) + single="$RUNNER_TEMP/link"; rm -rf "$single"; mkdir -p "$single"; cp "$exe" "$single/app" + cd "$single" + if [ "$RUNNER_OS" = Linux ]; then + xvfb-run -a timeout 120 ./app | tee out.txt + else + perl -e 'alarm 120; exec @ARGV' ./app | tee out.txt + fi + grep -q "link check ok" out.txt + + - name: "compose-rust/pythonx: config check on the generated metadata" + shell: bash + run: | + if [ "${{ steps.sample.outputs.awtfree }}" = true ]; then + bash extended/native-image/check-native-config.sh core-extended --strict "$SAMPLE_AWTFREE/src/main/native-image/reachability-metadata.json" + else + bash extended/native-image/check-native-config.sh core-extended extended/native-image/hello-awtfree-link/src/main/native-image/reachability-metadata.json + fi + + - name: Smoke start, Linux (Xvfb) + if: runner.os == 'Linux' && steps.sample.outputs.awtfree == 'true' + shell: bash + run: | + set -euo pipefail + dir="$SAMPLE_AWTFREE" + exe=$(find "$dir/build/compose/native-image" -maxdepth 2 -type f -perm -u+x | head -1) + single="$RUNNER_TEMP/single"; rm -rf "$single"; mkdir -p "$single"; cp "$exe" "$single/app" + cd "$single" + HELLO_SELF_CHECK="$single/self-check.png" xvfb-run -a timeout 120 ./app + test -s "$single/self-check.png" + + # No display session on the macOS runner to draw into, so this starts the executable and + # lets it quit on its own through the auto-exit variable, as compose-rust's smoke tests do. + - name: Smoke start, macOS (start and exit) + if: runner.os == 'macOS' && steps.sample.outputs.awtfree == 'true' + shell: bash + run: | + set -euo pipefail + dir="$SAMPLE_AWTFREE" + exe=$(find "$dir/build/compose/native-image" -maxdepth 2 -type f -perm -u+x | head -1) + single="$RUNNER_TEMP/single"; rm -rf "$single"; mkdir -p "$single"; cp "$exe" "$single/app" + cd "$single" + HELLO_SELF_CHECK="$single/self-check.png" perl -e 'alarm 120; exec @ARGV' ./app + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: native-image-${{ matrix.name }} + path: | + extended/native-image/*/build/tmp/packageNativeImage/**/*.log + extended/native-image/*/build/reports/ + if-no-files-found: ignore diff --git a/extended/native-image/check-native-config.sh b/extended/native-image/check-native-config.sh new file mode 100755 index 00000000000..f7eb460cec8 --- /dev/null +++ b/extended/native-image/check-native-config.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Checks that the window layer reaches native code only through the generated upcall table, +# and that the image's configuration has no runtime reflection for it. +# +# Usage: check-native-config.sh [--strict ...] +# +# Source checks, always: +# - no Class.forName, java.lang.reflect, kotlin.reflect or by-name method lookup in the +# window modules (extended/window, graalvm modules and common); +# - every file that declares a @CEntryPoint also builds a CEntryPointLiteral, so C reaches +# Kotlin through a table the image builder resolves, never through a name lookup. +# Kotlin/Native window modules (extended/window/native): no kotlin.reflect.full, ServiceLoader +# or by-name lookup of classes and selectors. +# Metadata checks, always, for each file given: +# - no reflection or jni entry names a class of org.thisisthepy.compose.window. +# With --strict (the AWT-free image) the metadata must also have: +# - no jni entry other than org.jetbrains.skiko classes (skiko's JNI is linked by stubs). +set -euo pipefail + +core="${1:?usage: check-native-config.sh [--strict] [metadata.json...]}" +shift +strict=0 +if [[ "${1:-}" == "--strict" ]]; then strict=1; shift; fi + +fail=0 +note() { echo "FAIL: $*" >&2; fail=1; } + +window="$core/extended/window" +[[ -d "$window" ]] || { echo "no $window" >&2; exit 2; } + +# Read line by line: macOS ships bash 3.2, which has no mapfile. +sources=() +while IFS= read -r line; do sources+=("$line"); done < <(find "$window" -type f \( -name '*.kt' -o -name '*.java' \) \ + -not -path '*/test/*' -not -path '*/build/*' | sort) +echo "window sources checked: ${#sources[@]}" +[[ ${#sources[@]} -gt 0 ]] || { echo "no window sources found" >&2; exit 2; } + +pattern='Class\.forName|java\.lang\.reflect|kotlin\.reflect|\.getDeclaredMethod\(|\.getMethod\(|\.getDeclaredField\(|MethodHandles\.lookup|ServiceLoader' +if hits=$(grep -nE "$pattern" "${sources[@]}" | grep -vE '^\S+:[0-9]+:\s*(//|\*|/\*)'); then + note "runtime reflection in the window sources:"; echo "$hits" >&2 +fi + +entry_files=0 +for f in "${sources[@]}"; do + if grep -q '@CEntryPoint' "$f"; then + entry_files=$((entry_files + 1)) + grep -q 'CEntryPointLiteral' "$f" || note "$f declares @CEntryPoint but builds no CEntryPointLiteral" + fi +done +echo "files with upcall entry points: $entry_files" +[[ $entry_files -gt 0 ]] || note "no @CEntryPoint found in the window sources; the upcall table is missing" + +# Kotlin/Native window modules: C and Objective-C reach Kotlin through staticCFunction or +# @CName entries, and nothing is looked up by name at run time. K/N has no JVM reflection, so +# what is left to forbid is kotlin.reflect.full, ServiceLoader and Objective-C class lookups by name. A selector for a target-action menu item is not one: it names a method of an object the code itself created. +kn_sources=() +while IFS= read -r line; do kn_sources+=("$line"); done < <(find "$window/native" -type f -name '*.kt' \ + -not -path '*/test/*' -not -path '*/build/*' 2>/dev/null | sort) +echo "Kotlin/Native window sources checked: ${#kn_sources[@]}" +if [[ ${#kn_sources[@]} -gt 0 ]]; then + kn_pattern='kotlin\.reflect\.full|ServiceLoader|Class\.forName|NSClassFromString|objc_getClass' + if hits=$(grep -nE "$kn_pattern" "${kn_sources[@]}" | grep -vE '^\S+:[0-9]+:\s*(//|\*|/\*)'); then + note "runtime lookup by name in the Kotlin/Native window sources:"; echo "$hits" >&2 + fi +fi + +for m in "$@"; do + [[ -f "$m" ]] || { note "no metadata file $m"; continue; } + echo "metadata: $m" + bad=$(jq -r '[(.reflection // [])[], (.jni // [])[]] | .[] | (.type // .name // "" | tostring) + | select(test("org\\.thisisthepy\\.compose\\.window"))' "$m") + [[ -z "$bad" ]] || note "reflection or jni entries for window classes in $m: $bad" + if [[ $strict -eq 1 ]]; then + other=$(jq -r '(.jni // [])[] | (.type // .name // "" | tostring) + | select(startswith("org.jetbrains.skiko") | not)' "$m") + [[ -z "$other" ]] || note "jni entries other than skiko's in $m: $other" + fi +done + +[[ $fail -eq 0 ]] && echo "native config check passed" +exit $fail diff --git a/extended/native-image/check-plugin-dsl-additive.sh b/extended/native-image/check-plugin-dsl-additive.sh new file mode 100755 index 00000000000..141723acaa8 --- /dev/null +++ b/extended/native-image/check-plugin-dsl-additive.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# The plugin's upstream DSL stays source compatible: against the upstream branch, the dsl +# directory may gain declarations and may not lose or change public ones. +# Usage: check-plugin-dsl-additive.sh [upstream ref, default origin/jb-main] +# The plugin has no binary-compatibility dump, so this compares declarations in the diff. +set -euo pipefail +base="${1:-origin/jb-main}" +dir=gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl +removed=$(git diff "$base"...HEAD -U0 -- "$dir" | grep -E '^-[^-]' \ + | grep -E '^-\s*((public|abstract|open|override|internal|data|sealed|enum|annotation)\s+)*(val|var|fun|class|interface|object)\s' \ + | grep -vE '^-\s*(private|internal)\s' || true) +if [[ -n "$removed" ]]; then + echo "FAIL: public DSL declarations removed or changed against $base:" >&2 + echo "$removed" >&2 + exit 1 +fi +echo "upstream DSL additive against $base" diff --git a/extended/native-image/hello-awtfree-link/build.gradle.kts b/extended/native-image/hello-awtfree-link/build.gradle.kts new file mode 100644 index 00000000000..c86a13e99f2 --- /dev/null +++ b/extended/native-image/hello-awtfree-link/build.gradle.kts @@ -0,0 +1,41 @@ +// The smallest AwtFree image that has to link the window layer: a plain main, no Compose +// scene, that opens one window through the extended window module, presents a frame and +// exits. If the Objective-C or C window source, or the upcall table, does not compile and +// link into the image, packageNativeImage here is where it shows. +plugins { + kotlin("jvm") version "2.2.20" + kotlin("plugin.compose") version "2.2.20" + id("org.jetbrains.compose") version "1.11.1-extended-dev" +} + +val macos = System.getProperty("os.name").startsWith("Mac") +val windowArtifact = if (macos) "graalvm-macos" else "graalvm-linux" + +sourceSets.main { + kotlin.srcDir(if (macos) "src/macos/kotlin" else "src/linux/kotlin") +} + +dependencies { + implementation("org.thisisthepy.compose.window:common:0.1.0") + implementation("org.thisisthepy.compose.window:$windowArtifact:0.1.0") + // skiko is linked into every image this plugin makes; the sample draws nothing with it. + implementation(compose.desktop.currentOs) +} + +compose.desktop.application { + mainClass = "link.MainKt" + windowing = org.jetbrains.compose.desktop.application.dsl.ApplicationWindowing.AwtFree + nativeImage { + // CORE_EXTENDED: a checkout of compose-multiplatform-core-extended; SKIKO_STATIC: the + // build-skiko-static-jvm.sh --no-jawt output. + providers.environmentVariable("CORE_EXTENDED").orNull?.let { windowSourcesDirectory.set(file(it)) } + // AppKit calls have to come from the process's first thread; GraalVM's launcher runs + // main on a new one on macOS unless told not to. + if (macos) buildArgs.add("-H:-RunMainInNewThread") + providers.environmentVariable("SKIKO_STATIC").orNull?.let { skikoStaticDirectory.set(file(it)) } + } +} + +// The window modules are built with a newer Kotlin than this sample; they use nothing the +// older metadata reader cannot follow, so the version check is skipped. +kotlin.compilerOptions.freeCompilerArgs.add("-Xskip-metadata-version-check") diff --git a/extended/native-image/hello-awtfree-link/gradle/wrapper/gradle-wrapper.jar b/extended/native-image/hello-awtfree-link/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..e6441136f3d Binary files /dev/null and b/extended/native-image/hello-awtfree-link/gradle/wrapper/gradle-wrapper.jar differ diff --git a/extended/native-image/hello-awtfree-link/gradle/wrapper/gradle-wrapper.properties b/extended/native-image/hello-awtfree-link/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..1a704683a00 --- /dev/null +++ b/extended/native-image/hello-awtfree-link/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/extended/native-image/hello-awtfree-link/gradlew b/extended/native-image/hello-awtfree-link/gradlew new file mode 100755 index 00000000000..1aa94a42690 --- /dev/null +++ b/extended/native-image/hello-awtfree-link/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/extended/native-image/hello-awtfree-link/settings.gradle.kts b/extended/native-image/hello-awtfree-link/settings.gradle.kts new file mode 100644 index 00000000000..3f1292fa1e9 --- /dev/null +++ b/extended/native-image/hello-awtfree-link/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + google() + } +} +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + google() + } +} +rootProject.name = "hello-awtfree-link" diff --git a/extended/native-image/hello-awtfree-link/src/linux/kotlin/Platform.kt b/extended/native-image/hello-awtfree-link/src/linux/kotlin/Platform.kt new file mode 100644 index 00000000000..5b61e45f5c0 --- /dev/null +++ b/extended/native-image/hello-awtfree-link/src/linux/kotlin/Platform.kt @@ -0,0 +1,6 @@ +package link + +import org.thisisthepy.compose.window.WindowPlatform +import org.thisisthepy.compose.window.graalvm.linux.X11Window + +fun windowPlatform(): WindowPlatform = X11Window() diff --git a/extended/native-image/hello-awtfree-link/src/macos/kotlin/Platform.kt b/extended/native-image/hello-awtfree-link/src/macos/kotlin/Platform.kt new file mode 100644 index 00000000000..2a437ecc5d7 --- /dev/null +++ b/extended/native-image/hello-awtfree-link/src/macos/kotlin/Platform.kt @@ -0,0 +1,6 @@ +package link + +import org.thisisthepy.compose.window.WindowPlatform +import org.thisisthepy.compose.window.graalvm.macos.AppKitWindowPlatform + +fun windowPlatform(): WindowPlatform = AppKitWindowPlatform() diff --git a/extended/native-image/hello-awtfree-link/src/main/kotlin/Main.kt b/extended/native-image/hello-awtfree-link/src/main/kotlin/Main.kt new file mode 100644 index 00000000000..0df4ea58ef3 --- /dev/null +++ b/extended/native-image/hello-awtfree-link/src/main/kotlin/Main.kt @@ -0,0 +1,29 @@ +package link + +import org.thisisthepy.compose.window.WindowConfig +import org.thisisthepy.compose.window.WindowEvent +import org.thisisthepy.compose.window.WindowListener +import kotlin.system.exitProcess + +/** + * Opens one window, pumps a few times, presents one frame and exits. It draws nothing: the + * point is that the window layer links and that its upcall table is reachable at run time. + */ +fun main() { + val platform = windowPlatform() + val listener = object : WindowListener { + override fun onEvent(event: WindowEvent) {} + } + if (!platform.open(WindowConfig(title = "AwtFree link check", width = 320, height = 200), listener)) { + System.err.println("link check failed: ${platform.name} could not open a window") + exitProcess(1) + } + repeat(20) { platform.pump(16) } + platform.requestFrame() + repeat(5) { platform.pump(16) } + val size = platform.measure() + println("link check: ${platform.name} measured $size") + platform.close() + println("link check ok") + exitProcess(0) +} diff --git a/extended/native-image/hello-awtfree-link/src/main/native-image/reachability-metadata.json b/extended/native-image/hello-awtfree-link/src/main/native-image/reachability-metadata.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/extended/native-image/hello-awtfree-link/src/main/native-image/reachability-metadata.json @@ -0,0 +1 @@ +{} diff --git a/extended/samples/hello-jvm/build.gradle.kts b/extended/samples/hello-jvm/build.gradle.kts new file mode 100644 index 00000000000..d9f271ae66b --- /dev/null +++ b/extended/samples/hello-jvm/build.gradle.kts @@ -0,0 +1,21 @@ +// The plain JVM way to ship: an ordinary Compose desktop application, packaged with jpackage +// into a self-contained app image. It is the baseline the native-image and Kotlin/Native +// samples are compared against. +plugins { + kotlin("jvm") version "2.2.20" + kotlin("plugin.compose") version "2.2.20" + id("org.jetbrains.compose") version "1.11.1-extended-dev" +} + +dependencies { + implementation(compose.desktop.currentOs) + implementation("org.jetbrains.compose.material3:material3:1.9.0") +} + +compose.desktop.application { + mainClass = "hello.MainKt" + nativeDistributions { + packageName = "hello-jvm" + packageVersion = "1.0.0" + } +} diff --git a/extended/samples/hello-jvm/gradle/wrapper/gradle-wrapper.jar b/extended/samples/hello-jvm/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..e6441136f3d Binary files /dev/null and b/extended/samples/hello-jvm/gradle/wrapper/gradle-wrapper.jar differ diff --git a/extended/samples/hello-jvm/gradle/wrapper/gradle-wrapper.properties b/extended/samples/hello-jvm/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..1a704683a00 --- /dev/null +++ b/extended/samples/hello-jvm/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/extended/samples/hello-jvm/gradlew b/extended/samples/hello-jvm/gradlew new file mode 100755 index 00000000000..1aa94a42690 --- /dev/null +++ b/extended/samples/hello-jvm/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/extended/samples/hello-jvm/settings.gradle.kts b/extended/samples/hello-jvm/settings.gradle.kts new file mode 100644 index 00000000000..3ff65f9f434 --- /dev/null +++ b/extended/samples/hello-jvm/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + google() + } +} +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + google() + } +} +rootProject.name = "hello-jvm" diff --git a/extended/samples/hello-jvm/src/main/kotlin/Main.kt b/extended/samples/hello-jvm/src/main/kotlin/Main.kt new file mode 100644 index 00000000000..65312dead78 --- /dev/null +++ b/extended/samples/hello-jvm/src/main/kotlin/Main.kt @@ -0,0 +1,64 @@ +package hello + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.unit.Density +import java.io.File +import kotlinx.coroutines.delay +import org.jetbrains.skia.EncodedImageFormat +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +@Composable +fun Content() { + MaterialTheme { + Surface(Modifier.fillMaxSize()) { + var count by remember { mutableStateOf(0) } + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Hello, Compose", style = MaterialTheme.typography.headlineMedium) + Button(onClick = { count++ }) { Text("Clicked $count times") } + } + } + } +} + +/** + * With HELLO_SELF_CHECK set to a path, the window opens, the same content is also drawn + * offscreen through Skia into a PNG at that path, and the application quits. It is how a + * build that nobody watches proves both that a window came up and that Skia drew text. + */ +fun main() { + val check = System.getenv("HELLO_SELF_CHECK") + application { + Window(onCloseRequest = ::exitApplication, title = "Hello JVM") { + Content() + if (check != null) { + LaunchedEffect(Unit) { + delay(1500) + val scene = ImageComposeScene(width = 480, height = 240, density = Density(1f)) { Content() } + val png = scene.render().encodeToData(EncodedImageFormat.PNG)!!.bytes + scene.close() + File(check).writeBytes(png) + exitApplication() + } + } + } + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeImageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeImageTask.kt index 8c7ad6eb0fe..a92699a262c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeImageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNativeImageTask.kt @@ -165,6 +165,16 @@ abstract class AbstractNativeImageTask : AbstractComposeDesktopTask() { args += "-H:ConfigurationFileDirectories=${it.absolutePath}" } args += link + if (!awt) { + // A CEntryPointLiteral is resolved while the image is built, so the classes that + // hold the upcall tables have to be initialized then. + val upcalls = when (platform) { + Platform.MacosArm64 -> listOf("AppKitUpcalls", "AppKitUpcallSlots").map { "org.thisisthepy.compose.window.graalvm.macos.$it" } + Platform.LinuxX64 -> listOf("org.thisisthepy.compose.window.graalvm.linux.X11Upcalls") + Platform.WindowsX64 -> emptyList() + } + if (upcalls.isNotEmpty()) args += "--initialize-at-build-time=${upcalls.joinToString(",")}" + } args += buildArgs.get() args += listOf("-o", output.resolve(imageName.get()).absolutePath, mainClass.get()) @@ -173,6 +183,7 @@ abstract class AbstractNativeImageTask : AbstractComposeDesktopTask() { // allows a command line through native-image's .cmd launcher. val argFile = workDir.resolve("native-image.args") argFile.writeText(args.drop(1).joinToString("\n") { quoteArgument(it) }) + logger.info("native-image arguments:\n" + args.drop(1).joinToString("\n")) run(listOf(args.first(), "@${argFile.absolutePath}"), workDir.resolve("native-image.log")) if (platform == Platform.LinuxX64) removeUnneededLinuxLibraries(output) logger.lifecycle("The executable is written to ${output.resolve(imageName.get())}") @@ -204,6 +215,8 @@ abstract class AbstractNativeImageTask : AbstractComposeDesktopTask() { for (framework in listOf("AppKit", "Carbon", "QuartzCore")) { linker("-framework", framework) } + // Skia and skiko's bindings are C++; with AWT the JDK's archives bring the library in. + linker("-lc++") } for (library in if (awt) listOf("awt_lwawt", "osxui", "skiko") else listOf("skiko")) { linker("-Wl,-exported_symbol,_JNI_OnLoad_$library")