diff --git a/.claude/skills/tag-dev/SKILL.md b/.claude/skills/tag-dev/SKILL.md new file mode 100644 index 000000000..3b62e478e --- /dev/null +++ b/.claude/skills/tag-dev/SKILL.md @@ -0,0 +1,65 @@ +--- +name: tag-dev +description: Create and push a timestamped dev tag for Nucleus 3.0 in the format v3.0.0-dev-YYYYMMDDHHMM, publishing runtime modules to Maven Central and the plugin to the Gradle Plugin Portal without running preMerge. Use when the user asks to "publish a dev build", "cut a dev", "tag dev", "publier une version dev", or similar on this project. +--- + +# Tag dev — Nucleus 3.0 + +Creates a timestamped dev tag on the current HEAD and pushes it to `origin`. The tag publishes +**unverified** artifacts: `.github/workflows/publish-maven.yaml` and `publish-plugin.yaml` skip +`preMerge` for dev tags, and the desktop / GraalVM release workflows ignore them entirely. + +## Format + +`v3.0.0-dev-YYYYMMDDHHMM` — e.g. `v3.0.0-dev-202609200830` for Sep 20 2026, 08:30 UTC. + +Timestamp components come from `date -u +%Y%m%d%H%M` (UTC, no separators, 12 chars). + +The published Maven version is the tag without the leading `v` (`3.0.0-dev-202609200830`), which +orders below `3.0.0` for Gradle and Maven, so a dev build can never shadow the real release. + +## Procedure + +1. **Verify the branch is on the 3.0 line** — `nucleus-2.6` (the 3.0 release is cut from it), + `nucleus-3.0` if it exists, or a feature branch cut from either. A dev tag on `main` or on the + 2.5 line would publish a `3.0.0-dev-*` version from the wrong code; abort and say so. +2. **Verify the working tree is clean** — `git status --porcelain` empty. If dirty, ask the user + whether to commit first or abort. Never tag a dirty tree: the tag is what CI builds. +3. **Verify HEAD is pushed** — `git fetch origin` then check the commit exists on the remote + (`git branch -r --contains HEAD`). A tag on an unpushed commit makes CI check out a commit + nobody else has; push the branch first (ask before pushing). +4. **Generate the timestamp** with `date -u +%Y%m%d%H%M`. +5. **Check the tag doesn't already exist** — `git tag -l v3.0.0-dev-`. If it does, use the + next minute; Maven Central versions are immutable, a retag would publish nothing. +6. **Create an annotated tag**: + ```bash + git tag -a "v3.0.0-dev-" -m "v3.0.0-dev-" + ``` + Annotated (not lightweight) because the published history uses annotated tags. +7. **Push the tag**: + ```bash + git push origin "v3.0.0-dev-" + ``` +8. **Report** the tag name, the commit SHA, the resulting Maven version, and the coordinates a + consumer needs, e.g.: + ```kotlin + implementation("dev.nucleusframework:nucleus.decorated-window-tao:3.0.0-dev-") + ``` + Mention that Central takes ~15 minutes to expose the version after the workflow goes green. + +## Hard rules + +- Never tag `main` or the 2.5 line with this format. +- Never cut a `2.6.0-dev-*` tag anymore: the next release is 3.0.0. +- Never overwrite or force-push a tag — the version is already on Central and cannot be replaced. +- Never add `Co-Authored-By` or AI attribution to the tag message (per the project's CLAUDE.md). +- Tag message body is just the tag name itself — matches the existing convention. +- The tag must stay `v..-dev-`: `.github/actions/release-tag-info` + rejects anything else, because every publish task derives its version by stripping + `refs/tags/v` from `GITHUB_REF`. + +## When NOT to use this skill + +- Stable releases (`v3.0.0`) — those go through the full `preMerge` gate and cut GitHub releases. +- Alpha/beta/rc prereleases — see the `tag-alpha` skill and `.github/actions/validate-release-ref`. +- Backporting onto an old commit — this skill always tags `HEAD`. diff --git a/.github/actions/build-macos-universal/action.yml b/.github/actions/build-macos-universal/action.yml index 0c8b06774..e60e18c5b 100644 --- a/.github/actions/build-macos-universal/action.yml +++ b/.github/actions/build-macos-universal/action.yml @@ -52,6 +52,14 @@ inputs: description: 'Path to runtime embedded.provisionprofile for sandboxed app runtime' required: false default: '' + electron-builder-toolchain-dir: + description: 'Directory holding the package.json / package-lock.json of the pinned electron-builder toolchain embedded in the Nucleus plugin' + required: false + default: 'plugin-build/plugin/src/main/resources/nucleus/electron-builder' + node-version: + description: 'Node.js line to provision (a major like 22, or a pinned x.y.z). Keep in sync with the nativeDistributions { nodejs { version } } default.' + required: false + default: '22' outputs: zip: @@ -102,6 +110,22 @@ runs: echo "==> No sandboxed ZIPs found (App Store PKG will use electron-builder fallback)" fi + # Same cache entry as setup-nucleus: the provisioning script uses the + # plugin's install layout, so a Node.js downloaded by either is reused. + - name: Cache Node.js toolchain + uses: actions/cache@v6 + with: + path: ~/.gradle/nucleus/nodejs + key: nodejs-toolchain-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }} + + - name: Provision Node.js and electron-builder + shell: bash + env: + NODE_LINE: ${{ inputs.node-version }} + TOOLCHAIN_DIR: ${{ inputs.electron-builder-toolchain-dir }} + TOOL_DIR: ${{ runner.temp }}/electron-builder-tool + run: bash "${{ github.action_path }}/provision-electron-builder.sh" + - name: Build universal binary id: build shell: bash diff --git a/.github/actions/build-macos-universal/build-universal.sh b/.github/actions/build-macos-universal/build-universal.sh index 445d22801..97e857651 100755 --- a/.github/actions/build-macos-universal/build-universal.sh +++ b/.github/actions/build-macos-universal/build-universal.sh @@ -4,7 +4,7 @@ set -euo pipefail # ── Required env vars ───────────────────────────────────────────────────── -: "${ARM64_ZIP:?}" "${X64_ZIP:?}" "${OUTPUT_DIR:?}" +: "${ARM64_ZIP:?}" "${X64_ZIP:?}" "${OUTPUT_DIR:?}" "${NODE_BIN:?}" "${ELECTRON_BUILDER_CLI:?}" # ── Optional env vars (default to empty) ────────────────────────────────── SIGNING_IDENTITY="${SIGNING_IDENTITY:-}" @@ -499,8 +499,9 @@ run_electron_builder() { codesign --force --deep --sign - "$app_copy" fi + # Provisioned by provision-electron-builder.sh from the plugin's lock file. CSC_IDENTITY_AUTO_DISCOVERY=false \ - npx --yes electron-builder \ + "$NODE_BIN" "$ELECTRON_BUILDER_CLI" \ --prepackaged "$app_copy" \ --config "$eb_dir/electron-builder.yml" \ --config.electronVersion=33.0.0 \ diff --git a/.github/actions/build-macos-universal/provision-electron-builder.sh b/.github/actions/build-macos-universal/provision-electron-builder.sh new file mode 100755 index 000000000..40e5bdc3f --- /dev/null +++ b/.github/actions/build-macos-universal/provision-electron-builder.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Provisions the same Node.js and the same pinned electron-builder toolchain the Nucleus plugin +# uses, for the universal repack — which runs electron-builder outside the plugin. +# +# - Node.js: the newest release of $NODE_LINE from nodejs.org, verified against the release's +# SHASUMS256.txt, installed with the plugin's layout (NodeToolchainProvisioner): +# /node--darwin-// plus a `.nucleus-provisioned` +# marker naming that top dir. A cache restored from a plugin run is therefore reused as is. +# - electron-builder: `npm ci --ignore-scripts` against the lock file embedded in the plugin, so +# every transitive package is checked against its recorded integrity hash (no `npx --yes`). +# +# Writes NODE_BIN and ELECTRON_BUILDER_CLI to $GITHUB_ENV. +set -euo pipefail + +: "${NODE_LINE:?}" "${TOOLCHAIN_DIR:?}" "${TOOL_DIR:?}" +# The plugin's default cache location (/nucleus/nodejs). +NODE_INSTALL_BASE="${NODE_INSTALL_BASE:-$HOME/.gradle/nucleus/nodejs}" + +case "$(uname -m)" in + arm64 | aarch64) arch="arm64" ;; + x86_64) arch="x64" ;; + *) echo "::error::Unsupported architecture $(uname -m)" >&2; exit 1 ;; +esac + +install_dir="$NODE_INSTALL_BASE/node-$NODE_LINE-darwin-$arch" +marker="$install_dir/.nucleus-provisioned" + +if [[ -f "$marker" && -x "$install_dir/$(cat "$marker")/bin/node" ]]; then + node_home="$install_dir/$(cat "$marker")" + echo "==> Reusing Node.js at $node_home" +else + dist="https://nodejs.org/dist" + # Same rule as the plugin: a pinned x.y.z as is, otherwise the newest release of that major. + if [[ "$NODE_LINE" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + version="v${NODE_LINE#v}" + else + version="$(curl -fsSL "$dist/index.json" | + jq -r --arg major "$NODE_LINE" \ + '[.[] | select(.version | startswith("v" + $major + "."))] + | sort_by(.version | ltrimstr("v") | split(".") | map(tonumber)) | last | .version')" + fi + if [[ -z "$version" || "$version" == "null" ]]; then + echo "::error::No Node.js release matches '$NODE_LINE'" >&2 + exit 1 + fi + + archive="node-$version-darwin-$arch.tar.gz" + work="$(mktemp -d)" + trap 'rm -rf "$work"' EXIT + + echo "==> Downloading Node.js $version from $dist/$version/$archive" + curl -fsSL -o "$work/$archive" "$dist/$version/$archive" + curl -fsSL -o "$work/SHASUMS256.txt" "$dist/$version/SHASUMS256.txt" + (cd "$work" && grep " $archive\$" SHASUMS256.txt | shasum -a 256 -c -) + + tar -xzf "$work/$archive" -C "$work" + top_dir="node-$version-darwin-$arch" + rm -rf "$install_dir" + mkdir -p "$install_dir" + mv "$work/$top_dir" "$install_dir/" + echo "$top_dir" > "$marker" + node_home="$install_dir/$top_dir" + echo "==> Node.js $version installed to $node_home" +fi + +node_bin="$node_home/bin/node" +# npm's launcher resolves `node` through PATH. +export PATH="$node_home/bin:$PATH" + +echo "==> Provisioning electron-builder from the plugin's lock file (npm ci --ignore-scripts)" +rm -rf "$TOOL_DIR" +mkdir -p "$TOOL_DIR" +cp "$TOOLCHAIN_DIR/package.json" "$TOOLCHAIN_DIR/package-lock.json" "$TOOL_DIR/" +(cd "$TOOL_DIR" && npm ci --ignore-scripts --no-audit --no-fund --no-progress --loglevel=error) + +cli="$TOOL_DIR/node_modules/electron-builder/cli.js" +if [[ ! -f "$cli" ]]; then + echo "::error::electron-builder CLI missing at $cli after npm ci" >&2 + exit 1 +fi +echo "==> electron-builder $("$node_bin" "$cli" --version)" + +{ + echo "NODE_BIN=$node_bin" + echo "ELECTRON_BUILDER_CLI=$cli" +} >> "$GITHUB_ENV" diff --git a/.github/actions/release-tag-info/action.yml b/.github/actions/release-tag-info/action.yml new file mode 100644 index 000000000..d285d5fa6 --- /dev/null +++ b/.github/actions/release-tag-info/action.yml @@ -0,0 +1,64 @@ +name: Release tag info +description: Classifies the pushed tag into a release channel and derives the version the build will publish + +outputs: + version: + description: Maven version the publish tasks will derive from the tag (tag name without the leading `v`) + value: ${{ steps.classify.outputs.version }} + channel: + description: '`dev` for a `v-dev-` tag, `release` for anything else' + value: ${{ steps.classify.outputs.channel }} + is-dev: + description: '`true` when the tag is a dev tag (publish without running preMerge)' + value: ${{ steps.classify.outputs.is-dev }} + +runs: + using: composite + steps: + - name: Classify tag + id: classify + shell: bash + run: | + set -euo pipefail + + if [[ "${GITHUB_REF_TYPE:-}" != "tag" ]]; then + echo "::error::release-tag-info only runs on tag refs (got '${GITHUB_REF_TYPE:-none}')." + exit 1 + fi + + tag="${GITHUB_REF_NAME}" + + # Every publish task derives its version with `GITHUB_REF.removePrefix("refs/tags/v")`, + # so a tag that is not `v` would publish a version literally named + # `refs/tags/`. Fail here rather than on Maven Central, where it is permanent. + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then + echo "::error::Tag '$tag' is not a publishable version tag (expected v..[-qualifier])." + exit 1 + fi + + version="${tag#v}" + + # Dev channel: `v2.6.0-dev-202609200830`. Cut from any branch, published without + # preMerge — the usual Kotlin-ecosystem `-dev-` build. + if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-dev([.-][0-9A-Za-z]+)*$ ]]; then + channel=dev + is_dev=true + else + channel=release + is_dev=false + fi + + echo "Tag '$tag' → version '$version', channel '$channel'." + { + echo "version=$version" + echo "channel=$channel" + echo "is-dev=$is_dev" + } >> "$GITHUB_OUTPUT" + + { + echo "### Publishing \`$version\` (\`$channel\` channel)" + if [[ "$is_dev" == "true" ]]; then + echo "" + echo "Dev tag: \`preMerge\` is skipped." + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/actions/setup-nucleus/action.yml b/.github/actions/setup-nucleus/action.yml index 242ccabc7..4dc785704 100644 --- a/.github/actions/setup-nucleus/action.yml +++ b/.github/actions/setup-nucleus/action.yml @@ -23,7 +23,7 @@ inputs: required: false default: 'false' setup-gradle: - description: 'Setup Gradle via gradle/actions/setup-gradle@v5' + description: 'Setup Gradle via gradle/actions/setup-gradle@v6 (open-source basic cache provider)' required: false default: 'true' graalvm: @@ -31,21 +31,17 @@ inputs: required: false default: 'false' graalvm-version: - description: 'GraalVM toolchain version, used only as a cache-key component. Keep in sync with the graalvm { toolchain { } } DSL (e.g. 25i3, 25).' + description: 'GraalVM toolchain version, used only as a cache-key component. Keep in sync with the graalvm { toolchain { } } DSL (e.g. 25i4, 25).' required: false - default: '25i3' + default: '25i4' graalvm-distribution: description: 'GraalVM distribution, used only as a cache-key component. Keep in sync with graalvm { toolchain { distribution } } (community or oracle). Changing it must not restore a cache holding the other distribution.' required: false default: 'community' - setup-node: - description: 'Setup Node.js' - required: false - default: 'true' node-version: - description: 'Node.js version' + description: 'Node.js line the Nucleus plugin provisions, used only as a cache-key component. Keep in sync with the nativeDistributions { nodejs { version } } DSL.' required: false - default: '24' + default: '22' outputs: java-home: @@ -62,10 +58,9 @@ runs: # Temurin, matching the rest of the workflows. jpackage bundles this JDK into # the distributed app. In GraalVM mode the native image is built entirely by # the plugin-provisioned GraalVM, so the Gradle JDK contributes nothing to - # that output. decorated-window-jbr needs no JBR here — the JBR API comes - # from the org.jetbrains.runtime:jbr-api Maven artifact. + # that output. - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: ${{ runner.os == 'Windows' && 'microsoft' || 'temurin' }} java-version: ${{ inputs.java-version }} @@ -94,7 +89,7 @@ runs: # a cache still holding the other build. - name: Cache GraalVM toolchain if: inputs.graalvm == 'true' - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.gradle/nucleus/graalvm key: graalvm-toolchain-${{ runner.os }}-${{ runner.arch }}-${{ inputs.graalvm-distribution }}-${{ inputs.graalvm-version }} @@ -152,11 +147,18 @@ runs: # ── Gradle ────────────────────────────────────────────────────────── - name: Setup Gradle if: inputs.setup-gradle == 'true' - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 + with: + # Open-source cache on GitHub Actions cache; v6's default provider is proprietary. + cache-provider: basic # ── Node.js ───────────────────────────────────────────────────────── - - name: Setup Node.js - if: inputs.setup-node == 'true' - uses: actions/setup-node@v6 + # No actions/setup-node: the Nucleus plugin downloads the Node.js it runs + # electron-builder with and caches it under ~/.gradle/nucleus/nodejs, so CI + # packages with the same toolchain a developer machine does. Only the cache + # is CI's business. + - name: Cache Node.js toolchain + uses: actions/cache@v6 with: - node-version: ${{ inputs.node-version }} + path: ~/.gradle/nucleus/nodejs + key: nodejs-toolchain-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }} diff --git a/.github/actions/validate-release-ref/action.yml b/.github/actions/validate-release-ref/action.yml index 55c74d701..4687552b7 100644 --- a/.github/actions/validate-release-ref/action.yml +++ b/.github/actions/validate-release-ref/action.yml @@ -3,13 +3,19 @@ description: Validate release tags that must be cut from a specific branch inputs: prerelease-branch: - description: Branch that owns 2.x prerelease tags + description: > + Branch that owns the prerelease tags. Empty (the default) derives it from the tag itself, + so the check follows the release line instead of going stale every cycle. required: false - default: nucleus-2.0 + default: '' + prerelease-branch-prefix: + description: Prefix of the derived prerelease branch name (`.`) + required: false + default: nucleus- prerelease-tag-regex: - description: Bash regex for prerelease tags that must be on prerelease-branch + description: Bash regex for prerelease tags that must be on the prerelease branch required: false - default: '^v2\.[0-9]+\.[0-9]+-(alpha|beta|rc)([.-][0-9A-Za-z]+)*$' + default: '^v[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta|rc)([.-][0-9A-Za-z]+)*$' runs: using: composite @@ -33,8 +39,23 @@ runs: exit 0 fi + # The branch that owns a prerelease tag is the one named after its release line: + # `v2.6.0-rc.1` belongs on `nucleus-2.6`. Derived rather than hard-coded, because a + # pinned branch name silently protects the *previous* line once the work moves on + # (the default was still `nucleus-2.0` while 2.6 was in development, and that branch + # no longer exists on origin — every rc would have failed to fetch). + if [[ -z "$prerelease_branch" ]]; then + line="${tag#v}" + line="${line%%-*}" + prerelease_branch="${{ inputs.prerelease-branch-prefix }}${line%.*}" + echo "Derived prerelease branch '$prerelease_branch' from tag '$tag'." + fi + echo "Validating prerelease tag '$tag' against origin/$prerelease_branch..." - git fetch --no-tags origin "+refs/heads/$prerelease_branch:refs/remotes/origin/$prerelease_branch" + if ! git fetch --no-tags origin "+refs/heads/$prerelease_branch:refs/remotes/origin/$prerelease_branch"; then + echo "::error::Prerelease branch '$prerelease_branch' does not exist on origin (derived from tag '$tag')." + exit 1 + fi if git merge-base --is-ancestor "$GITHUB_SHA" "refs/remotes/origin/$prerelease_branch"; then echo "Tag '$tag' points to a commit contained in origin/$prerelease_branch." diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..97ec799c0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,81 @@ +version: 2 + +# Grouped deliberately: ungrouped, this repository's dependency count would open +# dozens of pull requests the first week and a steady trickle after. One PR per +# group per week is reviewable; a major version bump still comes on its own, +# because that is the one that needs reading. +updates: + # Runtime libraries and the version catalog, including + # dev.nucleusframework:nucleus.angle-natives -- which is how a new ANGLE + # release branch reaches Nucleus. + - package-ecosystem: gradle + directory: / + schedule: + interval: weekly + day: monday + time: "06:00" + timezone: Europe/Paris + open-pull-requests-limit: 5 + groups: + kotlin-and-compose: + patterns: + - "org.jetbrains.kotlin*" + - "org.jetbrains.kotlinx*" + - "org.jetbrains.compose*" + - "org.jetbrains.androidx*" + update-types: [minor, patch] + angle: + patterns: + - "dev.nucleusframework:nucleus.angle-natives" + minor-and-patch: + patterns: ["*"] + exclude-patterns: + - "org.jetbrains.kotlin*" + - "org.jetbrains.kotlinx*" + - "org.jetbrains.compose*" + - "org.jetbrains.androidx*" + - "dev.nucleusframework:nucleus.angle-natives" + update-types: [minor, patch] + commit-message: + prefix: "chore(deps)" + + # The Gradle plugin is an included build with its own dependency graph. + - package-ecosystem: gradle + directory: /plugin-build + schedule: + interval: weekly + day: monday + time: "06:00" + timezone: Europe/Paris + open-pull-requests-limit: 3 + groups: + minor-and-patch: + patterns: ["*"] + update-types: [minor, patch] + commit-message: + prefix: "chore(deps)" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "06:00" + timezone: Europe/Paris + open-pull-requests-limit: 3 + groups: + actions: + patterns: ["*"] + commit-message: + prefix: "chore(ci)" + +# Deliberately not covered: +# +# - cargo (decorated-window-tao/src/main/native): the crate graph is pinned +# through [patch.crates-io] onto vendored, patched forks of tao and the +# accesskit crates. Dependabot would propose upgrades that silently drop +# those patches. +# - npm (plugin-build/plugin/src/main/resources/nucleus/electron-builder): the +# package.json / package-lock.json pair is generated by +# scripts/update-electron-builder-lock.sh and pinned on purpose, so the two +# would fight over the lockfile. diff --git a/.github/workflows/build-natives.yaml b/.github/workflows/build-natives.yaml index 40ba12956..ebe20bf67 100644 --- a/.github/workflows/build-natives.yaml +++ b/.github/workflows/build-natives.yaml @@ -18,18 +18,18 @@ jobs: timeout-minutes: 60 steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Restore native output cache id: natives-cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: '*/src/main/resources/nucleus/native/win32-*' key: natives-v1-windows-${{ hashFiles('**/src/main/native/**', '.github/workflows/build-natives.yaml') }} - name: Setup JDK 21 if: steps.natives-cache.outputs.cache-hit != 'true' - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' @@ -48,11 +48,6 @@ jobs: shell: cmd run: call native-ssl\src\main\native\windows\build.bat - - name: Build decorated-window-jni Windows DLLs - if: steps.natives-cache.outputs.cache-hit != 'true' - shell: cmd - run: call decorated-window-jni\src\main\native\windows\build.bat - - name: Build system-color Windows DLLs if: steps.natives-cache.outputs.cache-hit != 'true' shell: cmd @@ -125,19 +120,15 @@ jobs: shell: cmd run: call fs-watcher\src\main\native\windows\build.bat - - name: Build decorated-window-tao Windows DLLs + - name: Build share Windows DLLs if: steps.natives-cache.outputs.cache-hit != 'true' shell: cmd - run: call decorated-window-tao\src\main\native\windows\build.bat + run: call share\src\main\native\windows\build.bat - # ANGLE (libEGL + libGLESv2) backs the Tao Windows Direct3D-11 render path. - # Fetched from a pinned Electron release with SHA-256 verification (never - # committed — see .gitignore). Lands in the same win32-*/ dirs so it ships - # (and is cached) inside the natives-windows artifact. - - name: Fetch ANGLE runtime DLLs + - name: Build decorated-window-tao Windows DLLs if: steps.natives-cache.outputs.cache-hit != 'true' - shell: bash - run: bash decorated-window-tao/src/main/native/windows/fetch-angle.sh all + shell: cmd + run: call decorated-window-tao\src\main\native\windows\build.bat - name: Verify Windows natives shell: bash @@ -145,7 +136,6 @@ jobs: FILES=( "darkmode-detector/nucleus_windows_theme.dll" "native-ssl/nucleus_ssl.dll" - "decorated-window-jni/nucleus_windows_decoration.dll" "system-color/nucleus_systemcolor.dll" "energy-manager/nucleus_energy_manager.dll" "taskbar-progress/nucleus_taskbar_progress.dll" @@ -159,13 +149,12 @@ jobs: "media-control/nucleus_media_control_windows.dll" "spellcheck/nucleus_spellcheck.dll" "fs-watcher/nucleus_fs_watcher.dll" + "share/nucleus_share.dll" "decorated-window-tao/nucleus_tao.dll" "decorated-window-tao/nucleus_tao_windows_deco.dll" "decorated-window-tao/nucleus_tao_gl.dll" "decorated-window-tao/nucleus_tao_dnd.dll" "decorated-window-tao/nucleus_tao_windows_native_view.dll" - "decorated-window-tao/libEGL.dll" - "decorated-window-tao/libGLESv2.dll" ) MISSING=0 for arch in win32-x64 win32-aarch64; do @@ -182,7 +171,7 @@ jobs: if [ "$MISSING" = "1" ]; then exit 1; fi - name: Upload Windows natives - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-windows path: '*/src/main/resources/nucleus/native/win32-*/' @@ -193,18 +182,18 @@ jobs: timeout-minutes: 60 steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Restore native output cache id: natives-cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: '*/src/main/resources/nucleus/native/darwin-*' key: natives-v1-macos-${{ hashFiles('**/src/main/native/**', '.github/workflows/build-natives.yaml') }} - name: Setup JDK 21 if: steps.natives-cache.outputs.cache-hit != 'true' - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' @@ -217,14 +206,6 @@ jobs: if: steps.natives-cache.outputs.cache-hit != 'true' run: bash native-ssl/src/main/native/macos/build.sh - - name: Build decorated-window-jbr macOS dylibs - if: steps.natives-cache.outputs.cache-hit != 'true' - run: bash decorated-window-jbr/src/main/native/macos/build.sh - - - name: Build decorated-window-jni macOS dylibs - if: steps.natives-cache.outputs.cache-hit != 'true' - run: bash decorated-window-jni/src/main/native/macos/build.sh - - name: Build system-color macOS dylibs if: steps.natives-cache.outputs.cache-hit != 'true' run: bash system-color/src/main/native/macos/build.sh @@ -288,6 +269,10 @@ jobs: if: steps.natives-cache.outputs.cache-hit != 'true' run: bash fs-watcher/src/main/native/macos/build.sh + - name: Build share macOS dylibs + if: steps.natives-cache.outputs.cache-hit != 'true' + run: bash share/src/main/native/macos/build.sh + - name: Build decorated-window-tao macOS dylibs if: steps.natives-cache.outputs.cache-hit != 'true' run: bash decorated-window-tao/src/main/native/macos/build.sh @@ -301,8 +286,6 @@ jobs: FILES=( "darkmode-detector/libnucleus_darkmode.dylib" "native-ssl/libnucleus_ssl.dylib" - "decorated-window-jbr/libnucleus_macos.dylib" - "decorated-window-jni/libnucleus_macos_jni.dylib" "system-color/libnucleus_systemcolor.dylib" "energy-manager/libnucleus_energy_manager.dylib" "taskbar-progress/libnucleus_taskbar_progress.dylib" @@ -317,6 +300,7 @@ jobs: "media-control/libnucleus_media_control_macos.dylib" "spellcheck/libnucleus_spellcheck.dylib" "fs-watcher/libnucleus_fs_watcher.dylib" + "share/libnucleus_share.dylib" "decorated-window-tao/libnucleus_tao.dylib" "decorated-window-tao/libnucleus_tao_metal.dylib" "decorated-window-tao/libnucleus_tao_dnd.dylib" @@ -340,7 +324,7 @@ jobs: if [ "$MISSING" = "1" ]; then exit 1; fi - name: Upload macOS natives - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-macos path: '*/src/main/resources/nucleus/native/darwin-*/' @@ -367,18 +351,18 @@ jobs: arch: aarch64 steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Restore native output cache id: natives-cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: '*/src/main/resources/nucleus/native/linux-*' key: natives-v1-linux-${{ matrix.arch }}-${{ hashFiles('**/src/main/native/**', '.github/workflows/build-natives.yaml') }} - name: Setup JDK 21 if: steps.natives-cache.outputs.cache-hit != 'true' - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' @@ -391,10 +375,6 @@ jobs: if: steps.natives-cache.outputs.cache-hit != 'true' run: bash darkmode-detector/src/main/native/linux/build.sh - - name: Build decorated-window-jni Linux native shared library - if: steps.natives-cache.outputs.cache-hit != 'true' - run: bash decorated-window-jni/src/main/native/linux/build.sh - - name: Build linux-hidpi native shared library if: steps.natives-cache.outputs.cache-hit != 'true' run: bash linux-hidpi/src/main/native/linux/build.sh @@ -454,6 +434,10 @@ jobs: if: steps.natives-cache.outputs.cache-hit != 'true' run: bash fs-watcher/src/main/native/linux/build.sh + - name: Build share Linux native libraries + if: steps.natives-cache.outputs.cache-hit != 'true' + run: bash share/src/main/native/linux/build.sh + - name: Build decorated-window-tao Linux native libraries if: steps.natives-cache.outputs.cache-hit != 'true' run: bash decorated-window-tao/src/main/native/linux/build.sh @@ -462,7 +446,6 @@ jobs: run: | FILES=( "darkmode-detector/libnucleus_linux_theme.so" - "decorated-window-jni/libnucleus_linux_jni.so" "linux-hidpi/libnucleus_linux_hidpi_jni.so" "spellcheck/libnucleus_spellcheck.so" "system-color/libnucleus_systemcolor.so" @@ -476,6 +459,7 @@ jobs: "scheduler/libnucleus_scheduler_linux.so" "media-control/libnucleus_media_control_linux.so" "fs-watcher/libnucleus_fs_watcher.so" + "share/libnucleus_share.so" "decorated-window-tao/libnucleus_tao.so" "decorated-window-tao/libnucleus_tao_egl.so" "decorated-window-tao/libnucleus_tao_linux_widget.so" @@ -495,7 +479,7 @@ jobs: if [ "$MISSING" = "1" ]; then exit 1; fi - name: Upload Linux natives - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-linux-${{ matrix.arch }} path: '*/src/main/resources/nucleus/native/linux-*/' diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml index f4c29e5af..c616c5ff8 100644 --- a/.github/workflows/pre-merge.yaml +++ b/.github/workflows/pre-merge.yaml @@ -33,10 +33,10 @@ jobs: GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true @@ -54,14 +54,6 @@ jobs: "native-ssl/src/main/resources/nucleus/native/darwin-x64/libnucleus_ssl.dylib" "native-ssl/src/main/resources/nucleus/native/win32-x64/nucleus_ssl.dll" "native-ssl/src/main/resources/nucleus/native/win32-aarch64/nucleus_ssl.dll" - "decorated-window-jbr/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_macos.dylib" - "decorated-window-jbr/src/main/resources/nucleus/native/darwin-x64/libnucleus_macos.dylib" - "decorated-window-jni/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_macos_jni.dylib" - "decorated-window-jni/src/main/resources/nucleus/native/darwin-x64/libnucleus_macos_jni.dylib" - "decorated-window-jni/src/main/resources/nucleus/native/linux-x64/libnucleus_linux_jni.so" - "decorated-window-jni/src/main/resources/nucleus/native/linux-aarch64/libnucleus_linux_jni.so" - "decorated-window-jni/src/main/resources/nucleus/native/win32-x64/nucleus_windows_decoration.dll" - "decorated-window-jni/src/main/resources/nucleus/native/win32-aarch64/nucleus_windows_decoration.dll" "linux-hidpi/src/main/resources/nucleus/native/linux-x64/libnucleus_linux_hidpi_jni.so" "linux-hidpi/src/main/resources/nucleus/native/linux-aarch64/libnucleus_linux_hidpi_jni.so" "spellcheck/src/main/resources/nucleus/native/linux-x64/libnucleus_spellcheck.so" @@ -92,6 +84,12 @@ jobs: "fs-watcher/src/main/resources/nucleus/native/darwin-x64/libnucleus_fs_watcher.dylib" "fs-watcher/src/main/resources/nucleus/native/linux-x64/libnucleus_fs_watcher.so" "fs-watcher/src/main/resources/nucleus/native/linux-aarch64/libnucleus_fs_watcher.so" + "share/src/main/resources/nucleus/native/win32-x64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/win32-aarch64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/darwin-x64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/linux-x64/libnucleus_share.so" + "share/src/main/resources/nucleus/native/linux-aarch64/libnucleus_share.so" "notification-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_notification.dylib" "notification-macos/src/main/resources/nucleus/native/darwin-x64/libnucleus_notification.dylib" "service-management-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_service_management.dylib" @@ -152,10 +150,6 @@ jobs: "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/nucleus_tao_dnd.dll" "decorated-window-tao/src/main/resources/nucleus/native/win32-x64/nucleus_tao_windows_native_view.dll" "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/nucleus_tao_windows_native_view.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-x64/libEGL.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/libEGL.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-x64/libGLESv2.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/libGLESv2.dll" "decorated-window-tao/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_tao.dylib" "decorated-window-tao/src/main/resources/nucleus/native/darwin-x64/libnucleus_tao.dylib" "decorated-window-tao/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_tao_metal.dylib" @@ -193,13 +187,16 @@ jobs: if [ "$MISSING" = "1" ]; then exit 1; fi - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 + with: + # Open-source cache on GitHub Actions cache; v6's default provider is proprietary. + cache-provider: basic - name: Run Gradle tasks run: ./gradlew preMerge --continue @@ -221,22 +218,25 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 + with: + # Open-source cache on GitHub Actions cache; v6's default provider is proprietary. + cache-provider: basic - name: Run Tao scene tests run: ./gradlew :decorated-window-tao:test @@ -249,7 +249,9 @@ jobs: # three display-stack runners for nothing. tao-headful: needs: [build-natives, gradle, tao-tests] - timeout-minutes: 30 + # Above the suite's own 45 min watchdog (GLOBAL_WATCHDOG_MILLIS) plus setup + # and compile, so a hung run is halted with a thread dump, not killed here. + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -257,22 +259,25 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 + with: + # Open-source cache on GitHub Actions cache; v6's default provider is proprietary. + cache-provider: basic - name: Install Mesa software OpenGL (Windows) if: runner.os == 'Windows' @@ -280,7 +285,7 @@ jobs: # the Tao scene host cannot attach. Mesa's llvmpipe opengl32.dll, # dropped next to java.exe (first directory in the DLL search order), # gives every JVM-launched window a real software GL context. - uses: f3d-app/install-mesa-windows-action@v1 + uses: f3d-app/install-mesa-windows-action@v3 with: path: C:\hostedtoolcache\windows\Java_Temurin-Hotspot_jdk\*\x64\bin @@ -340,22 +345,25 @@ jobs: # TCC-trusted) and Linux (AT-SPI) verify the full round-trips in CI. steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 + with: + # Open-source cache on GitHub Actions cache; v6's default provider is proprietary. + cache-provider: basic # NOTE: a Mesa/llvmpipe opengl32.dll install step used to sit here. It # was a red herring: the Windows host renders through ANGLE/D3D11 (WARP diff --git a/.github/workflows/publish-maven.yaml b/.github/workflows/publish-maven.yaml index 19a408df1..665377b01 100644 --- a/.github/workflows/publish-maven.yaml +++ b/.github/workflows/publish-maven.yaml @@ -8,12 +8,20 @@ on: jobs: validate-release-ref: runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.tag.outputs.version }} + channel: ${{ steps.tag.outputs.channel }} + is-dev: ${{ steps.tag.outputs.is-dev }} steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 + - name: Classify release tag + id: tag + uses: ./.github/actions/release-tag-info + - name: Validate release ref uses: ./.github/actions/validate-release-ref @@ -23,14 +31,14 @@ jobs: uses: ./.github/workflows/build-natives.yaml publish: - needs: build-natives + needs: [validate-release-ref, build-natives] runs-on: ubuntu-22.04 steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true @@ -48,14 +56,6 @@ jobs: "native-ssl/src/main/resources/nucleus/native/darwin-x64/libnucleus_ssl.dylib" "native-ssl/src/main/resources/nucleus/native/win32-x64/nucleus_ssl.dll" "native-ssl/src/main/resources/nucleus/native/win32-aarch64/nucleus_ssl.dll" - "decorated-window-jbr/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_macos.dylib" - "decorated-window-jbr/src/main/resources/nucleus/native/darwin-x64/libnucleus_macos.dylib" - "decorated-window-jni/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_macos_jni.dylib" - "decorated-window-jni/src/main/resources/nucleus/native/darwin-x64/libnucleus_macos_jni.dylib" - "decorated-window-jni/src/main/resources/nucleus/native/linux-x64/libnucleus_linux_jni.so" - "decorated-window-jni/src/main/resources/nucleus/native/linux-aarch64/libnucleus_linux_jni.so" - "decorated-window-jni/src/main/resources/nucleus/native/win32-x64/nucleus_windows_decoration.dll" - "decorated-window-jni/src/main/resources/nucleus/native/win32-aarch64/nucleus_windows_decoration.dll" "linux-hidpi/src/main/resources/nucleus/native/linux-x64/libnucleus_linux_hidpi_jni.so" "linux-hidpi/src/main/resources/nucleus/native/linux-aarch64/libnucleus_linux_hidpi_jni.so" "spellcheck/src/main/resources/nucleus/native/linux-x64/libnucleus_spellcheck.so" @@ -86,6 +86,12 @@ jobs: "fs-watcher/src/main/resources/nucleus/native/darwin-x64/libnucleus_fs_watcher.dylib" "fs-watcher/src/main/resources/nucleus/native/linux-x64/libnucleus_fs_watcher.so" "fs-watcher/src/main/resources/nucleus/native/linux-aarch64/libnucleus_fs_watcher.so" + "share/src/main/resources/nucleus/native/win32-x64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/win32-aarch64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/darwin-x64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/linux-x64/libnucleus_share.so" + "share/src/main/resources/nucleus/native/linux-aarch64/libnucleus_share.so" "notification-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_notification.dylib" "notification-macos/src/main/resources/nucleus/native/darwin-x64/libnucleus_notification.dylib" "service-management-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_service_management.dylib" @@ -146,10 +152,6 @@ jobs: "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/nucleus_tao_dnd.dll" "decorated-window-tao/src/main/resources/nucleus/native/win32-x64/nucleus_tao_windows_native_view.dll" "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/nucleus_tao_windows_native_view.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-x64/libEGL.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/libEGL.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-x64/libGLESv2.dll" - "decorated-window-tao/src/main/resources/nucleus/native/win32-aarch64/libGLESv2.dll" "decorated-window-tao/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_tao.dylib" "decorated-window-tao/src/main/resources/nucleus/native/darwin-x64/libnucleus_tao.dylib" "decorated-window-tao/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_tao_metal.dylib" @@ -187,13 +189,16 @@ jobs: if [ "$MISSING" = "1" ]; then exit 1; fi - name: Setup JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '21' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 + with: + # Open-source cache on GitHub Actions cache; v6's default provider is proprietary. + cache-provider: basic - name: Capture runner egress IP run: | @@ -201,7 +206,11 @@ jobs: curl -s https://api.ipify.org; echo curl -s https://api64.ipify.org; echo + # Dev builds (`v2.6.0-dev-`) publish straight from the tag: the whole + # point is a throwaway version a downstream app can consume today. Every other tag + # is a real release and still has to pass preMerge. - name: Run pre-merge checks + if: needs.validate-release-ref.outputs.is-dev != 'true' run: ./gradlew preMerge --continue - name: Publish to Maven Central diff --git a/.github/workflows/publish-plugin.yaml b/.github/workflows/publish-plugin.yaml index 3dce77546..9ef425b77 100644 --- a/.github/workflows/publish-plugin.yaml +++ b/.github/workflows/publish-plugin.yaml @@ -8,12 +8,20 @@ on: jobs: validate-release-ref: runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.tag.outputs.version }} + channel: ${{ steps.tag.outputs.channel }} + is-dev: ${{ steps.tag.outputs.is-dev }} steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 + - name: Classify release tag + id: tag + uses: ./.github/actions/release-tag-info + - name: Validate release ref uses: ./.github/actions/validate-release-ref @@ -22,7 +30,7 @@ jobs: uses: ./.github/workflows/build-natives.yaml gradle: - needs: build-natives + needs: [validate-release-ref, build-natives] runs-on: ubuntu-22.04 env: GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} @@ -30,21 +38,34 @@ jobs: if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v7 - name: Install Linux build dependencies run: sudo apt-get update && sudo apt-get install -y libx11-dev libdbus-1-dev libglib2.0-dev - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true + # ubuntu-22.04 defaults to JDK 11; Gradle 9 needs 17+. + - name: Setup JDK 21 + uses: actions/setup-java@v6 + with: + distribution: 'temurin' + java-version: '21' + - name: Cache Gradle Caches - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 + with: + # Open-source cache on GitHub Actions cache; v6's default provider is proprietary. + cache-provider: basic + # Dev tags (`v2.6.0-dev-`) go out unverified on purpose; real + # release tags still have to pass preMerge before reaching the portal. - name: Run Gradle tasks + if: needs.validate-release-ref.outputs.is-dev != 'true' run: ./gradlew preMerge --continue - name: Publish on Plugin Portal diff --git a/.github/workflows/release-desktop.yaml b/.github/workflows/release-desktop.yaml index 98f15f3f6..5c948d0ac 100644 --- a/.github/workflows/release-desktop.yaml +++ b/.github/workflows/release-desktop.yaml @@ -3,7 +3,10 @@ name: Release Desktop App (All Platforms) on: push: tags: + # Dev builds (`v2.6.0-dev-`) only publish libraries to Maven — + # they must not cut a GitHub release or burn the per-OS packaging matrix. - "v*" + - "!v*-dev*" workflow_dispatch: permissions: @@ -18,7 +21,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -46,7 +49,6 @@ jobs: arch: amd64 - os: windows-11-arm arch: arm64 - node-version: '22' # npm 11 (Node 24) has ECOMPROMISED bugs on Windows ARM64 - os: macos-latest arch: arm64 - os: macos-15-intel @@ -59,7 +61,7 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -78,11 +80,9 @@ jobs: flatpak: 'true' snap: 'true' setup-gradle: 'true' - setup-node: 'true' - node-version: ${{ matrix.node-version || '24' }} - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true @@ -137,7 +137,7 @@ jobs: - name: Upload packaged artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-assets-${{ runner.os }}-${{ matrix.arch }} path: | @@ -181,18 +181,14 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v7 with: sparse-checkout: | .github/actions examples/nucleus-demo/packaging/macos + plugin-build/plugin/src/main/resources/nucleus/electron-builder fetch-depth: 1 - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - name: Setup macOS signing id: signing if: env.HAS_SIGNING_CERTS == 'true' @@ -213,13 +209,13 @@ jobs: fi - name: Download macOS arm64 artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: release-assets-macOS-arm64 path: artifacts/release-assets-macOS-arm64 - name: Download macOS amd64 artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: release-assets-macOS-amd64 path: artifacts/release-assets-macOS-amd64 @@ -290,7 +286,7 @@ jobs: run: security delete-keychain "${{ steps.signing.outputs.keychain-path }}" || true - name: Upload universal macOS artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-assets-macOS-universal path: artifacts/release-assets-macOS-universal @@ -305,7 +301,7 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v7 with: sparse-checkout: | .github/actions @@ -313,13 +309,13 @@ jobs: fetch-depth: 1 - name: Download Windows amd64 artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: release-assets-Windows-amd64 path: artifacts/release-assets-Windows-amd64 - name: Download Windows arm64 artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: release-assets-Windows-arm64 path: artifacts/release-assets-Windows-arm64 @@ -333,7 +329,7 @@ jobs: certificate-password: 'ChangeMe-Temp123!' - name: Upload Windows bundle artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-assets-Windows-bundle path: artifacts/release-assets-Windows-bundle @@ -352,13 +348,13 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@v7 with: sparse-checkout: .github/actions fetch-depth: 1 - name: Download all build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts pattern: release-assets-* diff --git a/.github/workflows/release-graalvm.yaml b/.github/workflows/release-graalvm.yaml index 43ccfd338..a88b08b07 100644 --- a/.github/workflows/release-graalvm.yaml +++ b/.github/workflows/release-graalvm.yaml @@ -3,7 +3,10 @@ name: Release GraalVM Native Image (Jewel Sample) on: push: tags: + # Dev builds (`v2.6.0-dev-`) only publish libraries to Maven — + # they must not cut a GitHub release or burn the per-OS packaging matrix. - "v*" + - "!v*-dev*" workflow_dispatch: permissions: @@ -18,7 +21,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -59,12 +62,12 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true @@ -74,7 +77,6 @@ jobs: with: graalvm: 'true' setup-gradle: 'true' - setup-node: 'true' - name: Configure Linux GPG signing if: runner.os == 'Linux' @@ -114,7 +116,7 @@ jobs: fi - name: Upload GraalVM artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: graalvm-jewel-${{ runner.os }}-${{ matrix.arch }} path: | @@ -133,7 +135,7 @@ jobs: steps: - name: Download all GraalVM artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts pattern: graalvm-jewel-* diff --git a/.github/workflows/test-graalvm.yaml b/.github/workflows/test-graalvm.yaml index 8338bbdda..55214fdaf 100644 --- a/.github/workflows/test-graalvm.yaml +++ b/.github/workflows/test-graalvm.yaml @@ -29,10 +29,10 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true @@ -42,7 +42,6 @@ jobs: with: graalvm: 'true' setup-gradle: 'true' - setup-node: 'false' # ── Stage 1: build + run the Tao native test pyramid ──────────────── # Compiles examples/tao-native-test, then executes the packaged binary @@ -58,14 +57,18 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y xvfb openbox mesa-utils libegl1 libgl1-mesa-dri libgtk-3-0 + # Xvfb itself comes from setup-nucleus, which also starts it. x11-utils provides + # xdpyinfo, which the next step uses to wait for and check the X server. + sudo apt-get install -y openbox mesa-utils x11-utils libegl1 libgl1-mesa-dri libgtk-3-0 - name: Run native test pyramid (Linux, under Xvfb + openbox) if: runner.os == 'Linux' run: | - export DISPLAY=:99 - Xvfb :99 -screen 0 1600x1200x24 & - for i in $(seq 1 50); do xdpyinfo -display :99 >/dev/null 2>&1 && break; sleep 0.2; done + # setup-nucleus (packaging-tools) already runs Xvfb and exports DISPLAY; a second + # `Xvfb :99` here only failed with "Server is already active for display 99". + test -n "${DISPLAY:-}" || { echo "::error::DISPLAY is not set; setup-nucleus should start Xvfb"; exit 1; } + for i in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; sleep 0.2; done + xdpyinfo -display "$DISPLAY" >/dev/null || { echo "::error::No X server on $DISPLAY"; exit 1; } openbox & sleep 1 # Use the PACKAGED binary: the plugin extracts skiko/jawt/icudtl @@ -80,6 +83,11 @@ jobs: - name: Run native test pyramid if: runner.os != 'Linux' shell: bash + env: + # Arms the macOS scrollWheel: injector the trackpad headful cases drive + # (nativeDiagInjectScrollWheel); without it they fail. taoHeadfulTest sets the + # same variable for the JVM run. + NUCLEUS_TAO_INPUT_INJECTION: '1' run: | # Use the PACKAGED binary: the plugin extracts skiko/jawt/icudtl # next to it, which the bare nativeCompile artifact lacks. @@ -101,7 +109,7 @@ jobs: - name: Upload GraalVM artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: graalvm-${{ runner.os }} path: | diff --git a/.github/workflows/test-packaging.yaml b/.github/workflows/test-packaging.yaml index 5335add5e..3b939c789 100644 --- a/.github/workflows/test-packaging.yaml +++ b/.github/workflows/test-packaging.yaml @@ -28,7 +28,6 @@ jobs: - name: Windows ARM64 os: windows-11-arm arch: arm64 - node-version: '22' # npm 11 (Node 24) has ECOMPROMISED bugs on Windows ARM64 - name: macOS ARM64 os: macos-latest arch: arm64 @@ -38,7 +37,7 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Nucleus uses: ./.github/actions/setup-nucleus @@ -47,11 +46,9 @@ jobs: flatpak: 'true' snap: 'true' setup-gradle: 'true' - setup-node: 'true' - node-version: ${{ matrix.node-version || '24' }} - name: Download native artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: 'natives-*' merge-multiple: true @@ -84,7 +81,7 @@ jobs: - name: Upload packaged artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-packages-${{ runner.os }}-${{ matrix.arch }} path: | diff --git a/.gitignore b/.gitignore index 907f4767b..3f5f98b4a 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,9 @@ examples/tao-demo/src/main/native/windows/build_log.txt **/graalvm/libraryMetadata/ **/graalvm/metadataRepoDirs.txt +# Compiled Python caches (local helper scripts) +__pycache__/ + # JVM crash logs hs_err_pid*.log replay_pid*.log diff --git a/CLAUDE.md b/CLAUDE.md index 5f8108ad3..37b1c8d8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,14 +2,14 @@ A multi-module Gradle plugin and runtime library toolkit for shipping production-ready JVM desktop applications on macOS, Windows, and Linux. -Published releases are `2.4.x` (latest tag `v2.4.4`). Do not treat `IDEAL_API.md` as current — that file is gone; the real entry point is `nucleusApplication(args) { }` in `nucleus-application`. Plugin-injected strings are `NucleusApp`, not a generated `NucleusGenerated` object. +Published releases are `2.5.x` (latest tag `v2.5.0`). Do not treat `IDEAL_API.md` as current — that file is gone; the real entry point is `nucleusApplication(args) { }` in `nucleus-application`. Plugin-injected strings are `NucleusApp`, not a generated `NucleusGenerated` object. ## Project Structure -- `nucleus-application` - `nucleusApplication`, backend-agnostic `DecoratedWindow` / `HostedWindow`, `onDeepLink`, `aotTraining` +- `nucleus-application` - `nucleusApplication`, `DecoratedWindow` / `HostedWindow`, `onDeepLink`, `aotTraining` - `core-runtime` - Executable type detection, single instance, deep links, platform detection, app metadata (`NucleusApp`) - `aot-runtime` - AOT cache mode detection for JDK 25+ (Project Leyden) -- `updater-runtime` - Auto-update engine (GitHub/S3), SHA-512, delta/blockmap, progress, update level, post-update events +- `updater-runtime` / `updater-testing` - Auto-update engine (GitHub/S3/local directory), SHA-512, delta/blockmap, progress, update level, post-update events, Windows NSIS hot update (see Development Notes), and update testing without publishing (see Development Notes); `updater-testing` ships `UpdateFeedServer`, the fault-injecting loopback release host - `freedesktop-icons` - Type-safe freedesktop Icon Naming Specification constants (shared by notification-linux and launcher-linux) - `sf-symbols` - Type-safe SF Symbols catalog - `notification-common` - Cross-platform notification DSL with per-platform option blocks @@ -30,22 +30,21 @@ Published releases are `2.4.x` (latest tag `v2.4.4`). Do not treat `IDEAL_API.md - `energy-manager` - Energy efficiency & screen-awake APIs - `autolaunch` - Start at login (Win32/MSIX/SMAppService/systemd/Flatpak portal) - `scheduler` / `scheduler-testing` - OS-scheduled background tasks (Task Scheduler / launchd / systemd) + test doubles -- `fs-watcher` - Native filesystem watcher +- `fs-watcher` - Native filesystem watcher over the Rust `notify` crate. One native watcher per `FsWatcher` (all registrations share it; events are routed back by root in `lib.rs`, so an inotify instance is per watcher, not per path — #571). The backend is handed the canonical root on macOS and Linux (one watch per real directory; inotify keys watches by inode and `notify` keeps one spelling per descriptor, so aliases must share it — Kotlin projects events back onto each registration's spelling), the registered spelling on Windows (`canonicalize()` yields `\\?\` paths there). On macOS the Rust side also feeds FSEvents through `FsEventsNormalizer` before the debouncer: FSEvents reports an inode's *accumulated* flags (a rename of an old file arrives as `Create`+`Rename`+`Modify` on the gone path), which otherwise folds renames into a bare `Created(new)` and deletes into `Modified` (#570). Renames the backend cannot pair are `Removed(old)` + `Created(new)`, never dropped; `Raw` delivery never emits `Moved` +- `share` - Native share sheet, the repo's first **Kotlin Multiplatform library** (inspired by `robius-share`, THIRD_PARTY_NOTICES §5). `commonMain` holds the API (`ShareSheet.share { title; text(); url(); file() }`, suspend, throws `ShareException(ShareError)`); Android (`Intent.ACTION_SEND` chooser), iOS (`UIActivityViewController`) and the web (`js` + `wasmJs` sharing `webMain`, Web Share API through `js()` bodies so one source compiles for both; needs a user gesture, file items must be fetchable URIs) are pure Kotlin, only the `jvm` target goes through the Rust JNI crate (Windows `DataTransferManager` interop, macOS `NSSharingServicePicker`, Linux XDG portal over a hand-rolled D-Bus client). The KMP module keeps the JVM JNI layout (`src/main/native`, `src/main/resources` added to `jvmMain`) so CI globs stay uniform; `nucleus.native-module` wires `jvmProcessResources` when the multiplatform plugin is applied (target must be named `jvm`). Android files are copied under `cacheDir/nucleus-share` and served by the library's own `NucleusShareProvider` (`${applicationId}.nucleus.share`, no AndroidX), which also tracks the resumed activity. Desktop parents: `ShareSheet.share(request, ShareParent.Windows(hwnd) / MacOs(nsWindow, anchor) / Linux(portalParent, keepAlive))`; apps on `nucleus-application` call `NucleusWindow.share(request, anchor)` (`compileOnly` dependency, like FileKit), which resolves them from the Tao window. A Wayland `xdg_foreign` export must outlive the portal dialog while `share` returns before it closes: `keepAlive` is handed to the crate as a `java.lang.Runnable` (`Completion`, run on `Drop`, so every path releases it) that the Linux portal thread drops once the portal answers; the Kotlin side closes it itself (once) when the share fails or is cancelled before the native call. Kotlin 2.4 compiles the iOS klibs on any host, so iOS code is checked on Windows too; in a KMP module the aggregate `detekt` is NO-SOURCE — the root build wires the per-source-set `detekt*MainSourceSet` tasks into `check` - `service-management-macos` - macOS `SMAppService` — login items, launch agents, daemons - `native-ssl` / `native-http` / `native-http-okhttp` / `native-http-ktor` - OS trust store integration - `linux-hidpi` - Native HiDPI scale detection on Linux - `graalvm-runtime` - GraalVM native-image bootstrap - `decorated-window-core` - Shared types, layout, styling (design-system agnostic) -- `decorated-window-tao` - **Default/recommended backend** — no-AWT window shell over the Rust `tao` crate via JNI (Metal on macOS, EGL on Linux, ANGLE/GLES on Windows), single native event-loop thread as `Dispatchers.Main` -- `decorated-window-awt` - AWT chrome shared by the JBR/JNI backends -- `decorated-window-jbr` - JBR-based implementation (requires JetBrains Runtime) — **legacy/maintenance-only** -- `decorated-window-jni` - JNI-based implementation (any JVM, GraalVM compatible) — **legacy/maintenance-only** +- `decorated-window-tao` - **The only window backend** — no-AWT window shell over the Rust `tao` crate via JNI (Metal on macOS, EGL on Linux, ANGLE/GLES on Windows), single native event-loop thread as `Dispatchers.Main`. **Linux/Wayland frame timing (#444)**: Tao's GTK `draw` and `configure-event` handlers only post to its event channel, so `RedrawRequested` / `Resized` reach the host *after* GDK has already committed the toplevel; during a resize burst `TaoComposeSceneHostLinux` therefore renders from a real `draw` hook (`nativeConnectToplevelDraw`, size read with `gtk_window_get_size`), with the content sub-surface in `set_sync` and swap interval 0, and waits for the swap before returning so GTK's commit carries geometry and content together. Mesa applies `wl_egl_window_resize` only while no back buffer is acquired — push it before `eglMakeCurrent`, never after. The in-frame path is **watched**: GTK only paints while the compositor feeds GDK's frame clock, and Mutter sends no frame callback to a toplevel fully covered by its own opaque content sub-surface (maximized / tiled, no shadow ring) — so a `queue_draw` unanswered for 50 ms (`IN_FRAME_DRAW_GRACE_NS`, watchdog redraw via `DelayScheduler`) drops the burst back to the event-loop render path, and the burst's end (`endResizeBurstIfStale`) runs from `onRedrawRequested` too, never only from a render. Two more rules from the same finding: the in-frame path is never armed while the window is maximized / tiled / fullscreen (`parentObscured()`), and the content's opaque region (`nativeSetOpaqueRegion`) always leaves its **bottom row** out — a toplevel fully covered by an opaque subsurface is culled by Mutter, gets no frame callback, GDK's frame clock freezes and with it the flush-events phase that delivers pointer motion (GDK3 holds a lone motion event until that phase): the app renders at full rate but hover and drags only move when another event arrives +- `decorated-window-tao` internals: `window/tao/workspace/` is the shared, `internal` core behind the multi-window archetypes — `WindowGroup` (membership, focus recency, pinning), `RelocatedContentHost` + `RelocatingSaveableStateRegistry` (`rememberSaveable` state that follows content between windows), `HostGeometry` (drop targets in physical screen px), `CrossWindowDrag` (one live drag, screen-space drag handle), `DragGhostWindow`, `ScreenPlacement` (the public capability is `TaoWindow.canPlaceOnScreen` — the native-Wayland gate — GDK reports every toplevel at `(0, 0)` and ignores moves, so anything that treats `outerBoundsPx()`'s origin as a screen coordinate must check it; the size half stays valid there; `warnScreenPlacementUnsupported` logs the gap once per process), `TransferDrag` (the native-Wayland path of every cross-window gesture: the grip starts a platform **drag-and-drop** session carrying an in-process token (`TaoPrivateTransfer`, `SAME_APP` only), the window under the pointer resolves the drop in its *own* coordinates and records it on the session, and the source acts on that record when the session ends — inverted roles versus `ScreenDrag`, because the source is told nothing about where the pointer is; the drag icon is a reduced snapshot of the dragged palette or panel, taken through `TaoWindow.contentSnapshot`). **Tab drag, two paths.** Where the app places its windows the gesture is `screenDragHandle` → `TabWorkspace.beginDrag` (ghost window, screen hit-test, tear-off; the drop resolves through `dropTargetAt(draggedScreenRectPx, pointerScreenPx, …)` — a strip the **card** has reached counts as entered, the pointer's own strip still winning, the same rule as the dock zones, and a single-tab window's drag hands its own strip band as the card), and the strip animates the reorder from `dragPointerScreenPx`. Where it cannot (native Wayland), the grip is `tabStripLocalDragHandle`: a **local** reorder driven by the pointer's travel in window px and resolved by `reorderTarget` (edge-crossing, RTL inferred from the slots), and the moment the pointer leaves the strip the gesture is handed to the platform's drag-and-drop session — `transferDragHandle(gesture = …)` takes a `TransferDragGesture` whose `onDrag` returns `true` to start it mid-gesture, from the *press* position (Compose refuses a point outside the source node). That handover is what gives every *other* window the pointer in its own coordinates, so their strips can preview the drop; nothing else can, since a client hears nothing about a pointer another window holds. `DragGhostWindow(popupFor = source)` is the preview that follows the pointer out of a compositor-placed window (`wl_subsurface`, parent-relative positions). The tab slot carries `noWindowDrag()`: the title bar's move is a compositor grab that swallows the gesture. **`TabStrip` motion** (`TabStripAnimation.kt`, a port of `sh.calvin.reorderable`'s `ReorderableRow` state machine): items are `key`ed on the tab id; a tab dragged along its **own** strip publishes no ghost (`TabTearOffDragSession` clears it while `dropPreview.group === entry.group`) and the strip draws it at the pointer's travel since the grab (`TabWorkspace.dragGrabScreenPx` / `dragPointerScreenPx`), a neighbour slides one tab-width aside (spring `StiffnessMediumLow`) when the carried tab's *edge* crosses its *centre*, and on release the session sets `pendingReorder` instead of reordering — the strip's `TabStripMotion.settle` slides the tab into the target slot, then `reorder()` + `rest()` in the same frame, so nothing jumps. The own-strip drop index is `reorderTarget` (edge-crossing rule, RTL inferred from the slots, same rule as the motion) and `insertionIndex` is direction-aware too (a right-to-left strip used to resolve every drop mirrored); both take the direction from the strip's published `HostGeometry.layoutDirection` and infer it from the slot order only without one — a single tab cannot tell, and a right-to-left strip resolved left to right opens the preview on the wrong side, which moves the tab under the pointer and flips the index with every sample (two cards sliding about; `tab motion a sweep over a single-tab right-to-left strip` guards it). Offsets are draw-time `graphicsLayer` translations, so `tabSlot` geometry is always the settled layout. Tabs open/close by width (`AnimatedVisibility`, 200 ms, `clip = false` so the carried card can leave its slot) and the stock close button delays `workspace.close` by the exit duration; `TabEntry.isEntering` marks a tab the strip has not shown yet. `TabWindows` has two app slots: `windowWrapper` wraps the whole window *including* its strip (per-window locals, background), `windowBodyWrapper` wraps only what is under the strip and is where window-level chrome goes (a `DockLayout`, activity bars) — composed at one call site for every window, so a tab change neither rebuilds it nor moves the body's relocation keys. `SatelliteWorkspace` (docking) and `TabWorkspace` (Chrome-like tabs) are both built on it — put new cross-window gestures there rather than duplicating the geometry or the drag bookkeeping. `DockLayout` (`window/tao/DockLayout.kt` + `DockSplitter.kt` + `DockTransferTarget.kt`) is the dock: sides nest in `sideOrder` (outermost first, default `DefaultDockSideOrder` = top, bottom, left, right — **not** `DockSide.entries`, whose declaration order is left, right, top, bottom), a side is either *split* (panels share its length by `Docked.weight` and its thickness by `dockExtent(side)`) or *layered* (`layeredSides`: each panel a full-length layer of its own `Docked.extent`, the way a nested split-pane tree looks), `splitter` / `panel` slots carry the app's own chrome (`DockSplitterScope.dockSplitterHandle()` is the gesture; an overflowing `requiredWidth` grip on a 1 dp line works), sides are physical and the layout forces LTR internally then restores the caller's direction for content/panels/slots, and every panel and the content are `movableContentOf` so no layout change (extent, weight, order, side, restore, side order, direction) rebuilds a subtree — the layout's inputs live in `DockLayoutState` as snapshot state because the bands are separate composables that strong skipping would otherwise skip. Extents are fitted proportionally when the window is too small (`fit`). Drop feedback lives in `DockZoneHints.kt` and **the rectangles it draws are the target**: it publishes them to `HostGeometry.zoneBoundsInWindowPx`, and `dockTargetAt(draggedScreenRectPx, pointerScreenPx)` → `dockSideEntered` resolves a drop against those, not against the window's edges — on a layered side the strip is inset behind the existing layers, and the window's own edge behind them is nothing. A zone is entered when the dragged **satellite's** edge (its window, or the tear-out ghost) is within one zone thickness of the zone's outer edge and overlaps it across the other axis — edge alignment, not overlap, or a full-height panel could never be torn out; the pointer inside a zone is a second trigger and the tie-break, else the smallest gap wins. The rects come from `DockLayoutState.landingRectPx`: the side's measured band, inside existing layers, counting the dragged panel's own side as already freed; `hintedSides` drops the side the panel is alone on in that window, so it is neither drawn nor droppable. **`dockSides`**: `Satellite(dockSides = …)` (default all four, empty = floating-only) is fixed at declaration and enforced everywhere — `dock()` and `restore()` refuse another side, `hintedSides` and `DockZoneHints` neither draw nor publish it, the drag sessions resolve through `dockTargetFor(entry, …)` and the Wayland target filters on `drag.entry.dockSides`, and the default header hides its Dock action for a floating-only palette. **`floatable = false`** is the opposite knob — a fixed panel: `undock()` refuses it, a `restore()` that floats it is ignored, the docked drag publishes no tear-out ghost and a release off every zone leaves it in place, the default header drops its Float action, and the declaration requires a docked `initialPlacement`. **`minExtent` / `maxExtent`** (`Satellite(...)`, default `MinDockExtent`..∞) bound a panel's docked thickness: `SatelliteEntry.extentRange` clamps its own layer (`setDockedExtent`), `sideExtentRange(side, joining)` (thickest minimum, thinnest maximum, minimum wins) clamps a split side's shared extent (`setDockExtent`, re-applied by `dock()` and `restore()` so a newcomer's limits count), `dockSeedExtent` / `plannedDockExtent` are clamped so the preview is the width the drop produces (the split-side landing rect is the stack at that thickness, fitted like the panels), and `clampThicknessPx(…, panel)` stops the splitters; the floating window is not constrained. **`reorderable = false`** pins the rank: `dock(order)` is ignored for it (it takes the declared rank back), `insertInStack` pushes any other panel past the last pinned one (`pinnedFloor`), `dropSlotsPx` returns nothing for a pinned dragged panel and keeps the forbidden ranks as **empty** slots so a slot's index is still its rank, `hintedSides` drops its own side, `targetFor` strips the rank off a target, and `satelliteDragHandle` is inert when `canBeDragged` says a drag could not end anywhere. **Telling the two gestures apart** (what an app adapts its UI to, #663 review): `TaoWindow.canPlaceOnScreen` is the public capability (branch on it, not on `isNativeWaylandSurface`), `SatelliteScope.isCompositorPlaced` is the same answer for the window the chrome is composed in (the floating scope reads the satellite's own window through a lambda since the scope outlives it; the docked scope reads `entry.dockHost`), `SatelliteCaptionStripWidth` + the `floatingCaption` slot of `Satellite` are the strip the title bar leaves to the compositor's move — reserved and composed **only** where `isCompositorPlaced`, so an app never has to guess a width or accidentally claim the only area that can move the palette — and `SatelliteWorkspace.dragKind` (`Window` / `Transfer`) says how a drag in flight is carried, which is what tells preview code whether `dragGhost` will ever be published. `reader-dock-demo`: the book tree and the contents are `floatable = false` + `reorderable = false` + `dockSides = setOf(Right)` — furniture, and no pane can be dropped in front of them. **Ranks**: `Docked.order` is kept contiguous from 0 per (host, side) by `dock()` / `undock()` (`dock(order)` inserts at that index, `null` = the rank the entry last held on that side, remembered in `SatelliteEntry.dockMemory`, else the end), and a side with panels publishes `DockDropZone.slots` — one rect per rank, cut at the neighbours' centres, the dragged panel excluded — so `DockTarget.order` is the rank under the pointer (`dockSlotAt`), the own rank (`ownTarget`) being no target; a pointer over a stack beats a strip across its corner. `dropAt` converts a shown-rank into the full rank (closed panels keep theirs). The Wayland DnD path (`DockTransferTarget`) hit-tests the same published zones. A hand-driven `beginDrag` session must wait for the zones to be published before its first sample, or it resolves against the bare edges. `dock()` and the preview share one width (`dockSeedExtent`) and one weight (`dockSeedWeight`), so what lights up is what the release produces. **One drop preview everywhere** (`DragPreviewDefaults.kt`): the card that follows the pointer (`SatelliteGhostCard` / `TabGhostCard` on `DragPreviewSurface`) is also drawn on the space the release fills — the dock draws it at `DockLayoutState.dropRectPx(side, dragged, order, extentPx)` (empty side: the edge strip; layered: the layer at that rank; split: the share the re-divided weights give it, dividers counted), the tab strip opens a slot of the dragged tab's width (`TabStripScope.dropGhost` → `TabDropGhost`, `TabDropGhostCard`; `TabWorkspace.draggedTabWidth` reads the source slot) — and the sides merely on offer are the same surface at `hint` intensity. No insertion bars, no drop-indicator lines; a custom strip draws `dropGhost` itself, as `jewel-tabs-demo` does with a placeholder `TabData.Editor`. The card under the pointer is the `dragGhost` slot of `TabWindows` (default `TabDragGhostCard`) and the card in the landing slot is `TabStrip(dropGhostCard)` (default `TabDropGhostCard`; `TabDropGhost.tab` is the entry); both defaults are the public `TabGhostCard(tab, modifier)`, the shape an app's own card takes so one composable serves both slots. `WorkspaceDragKind` (`Window` / `Transfer`) is shared by `SatelliteWorkspace.dragKind` and `TabWorkspace.dragKind`, `Transfer` only once the platform session exists (a tab merely held in its strip on Wayland reports `null`); on native Wayland `dragGhost` is never published and the slot never composes. The ghost is laid out in the **source strip's / dock's** direction, for both archetypes: `publishHostGeometry` records `LocalLayoutDirection` on `HostGeometry.layoutDirection`, the drag session copies it onto `TabDragGhost` / `DragGhost`, and `DragGhostWindow(layoutDirection)` provides it in the ghost scene (a scene of its own re-provides the global direction over the bridged locals). The ghost content has the ghost window's `TaoDecoratedWindowScope`; `nucleus-application` wraps the `dragGhost` slot in `bindNucleusContent` (Nucleus locals, ghost direction) but never in the app's `windowWrapper`, which paints a window background. `DragController.active` is snapshot state so `dragKind` is observable. `TabWindows` and `DragGhostWindow` are `@ComposableOpenTarget(-1)` with `@UiComposable` lambdas (#636 — the inferred target flipped with incremental compilation, so it is pinned). `TabStrip` has per-tab `tabLeading` / `tabTrailing` slots (`Modifier.slotGap`: the 6 dp gap is charged only when the slot measured wider than 0, so the stock chip is unchanged and an empty slot costs nothing), the drop slot is sized by the strip whatever `dropGhostCard` draws, a tab opens/closes clipped to its slot only while `MutableTransitionState.isIdle` is false (AnimatedVisibility's own `clip` stays on for good and would cut a carried card), `TabPreview(tab, modifier)` is the one composable for a tab's thumbnail (the stock hover card is built on it), and `TabEntry.thumbnail` is app-assignable — assign before the tab is shown, from an effect next to the `Tab` declaration, since `restore()` creates no entries and the recorder's capture replaces whatever is there when the tab is shown. Headful coverage: `DockLayoutHeadfulCases` (robot splitter drags) + `DockLayoutMonkeyHeadfulCases` (profiles × seeds, `-Dnucleus.tao.headful.filter="dock layout"`). - `decorated-window-jewel` - Jewel (IntelliJ theme) integration - `decorated-window-material2` - Material 2 color mapping - `decorated-window-material3` - Material 3 color mapping - `plugin-build/plugin` - Gradle plugin for packaging & distribution - `buildSrc` - Build-only convention plugins (`nucleus.native-module`: the shared `buildNative*` wiring for every JNI module) -- `examples/` - Demo & sample applications: `nucleus-demo` (flagship), `compose-demo`, `tao-demo`, `swing-tao-demo`, `jni-demo`, `jewel-demo`, `cmp-demo` (KMP), `window-scaffold-demo`, `zstd-demo`, `scheduler-demo`, `service-management-demo`, `system-info-demo`, `fs-watcher-smoke`, `orphan-reflect-smoke`, `extra-launcher-demo`, `tao-native-test` (GraalVM + SLF4J fixture), `benchmark-demo` (JIT-vs-GraalVM-O3, ports under `ports/`), `gstreamer-demo` / `mediafoundation-demo` / `avfoundation-demo` (platform video into a `TextureView`), plus `shared` (Compose helper used by tao/jni demos). `native-proxy` and `spellcheck` directories on disk are **not** on `main` — ignore them unless the matching feature branch is checked out. +- `examples/` - Demo & sample applications: `nucleus-demo` (flagship), `compose-demo`, `tao-demo`, `swing-tao-demo`, `jewel-demo`, `cmp-demo` (KMP), `window-scaffold-demo`, `satellite-demo` (satellite workspace: floating palettes following the focused document, docking into a `DockLayout`, drag-to-dock, layout snapshots), `tabs-demo` (Chrome-like tabs: tear-off, merge, reorder, state following a tab between windows, layout snapshots), `jewel-tabs-demo` (the same tab workspace wearing Jewel's `TabStrip` / `TabData.Editor` chrome), `tab-satellites-demo` (the two archetypes composed: one `SatelliteWorkspace` per tab window, palettes drawing the window's selected tab), `reader-dock-demo` (a right-to-left book reader composing **both** archetypes: its seforim are tabs and its every pane is a satellite — layered right side with per-pane widths, `sideOrder` putting the right side outside the bottom one, the reader's own 1 dp + 5 dp-grip splitters and hover headers, Classic/Islands styles, one dock per tab window hung on `TabWindows(windowBodyWrapper)` so the strip stays the top of the window and a tab change touches no panel — the target layout of SeforimApp), `share-demo` (every `ShareSheet` payload on the desktop bridge; `-Dshare.demo.auto=