Skip to content
Merged
185 changes: 185 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
name: release
# One-click install: when a release is published, this builds labconstrictor-qupath-<tag>.jar with build.sh (Java 25, against the pinned QuPath),
# runs a smoke test on that exact jar in a real QuPath on a virtual screen (the menu entry exists, the dialog opens), and attaches the jar,
# LabConstrictorTools-all.groovy and SHA256SUMS to the release; the QuPath version and the minimum Tools version are appended to the notes.
# Manually (workflow_dispatch, input "tag") or on a pull request that changes this machinery it is a dry run: build, smoke test, files kept as workflow artifacts, nothing is published.
on:
pull_request: # a dry run when the release machinery itself changes (never publishes)
paths: [".github/workflows/release.yml", "build.sh", "tests/smoke_jar.*"]
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Tag name to build for (dry run: nothing is published)"
required: true
default: "v0.0.0-dryrun"
permissions:
contents: read
env:
# QuPath is pinned by version and checksum (same pin as qupath-gui.yml); the download is cached per QUPATH_VERSION.
QUPATH_VERSION: "0.7.0"
QUPATH_SHA256: 165e27a0731d58ba039e9d0d34a54acf896bb92e81922370df295cb85418ce53
TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag || 'v0.0.0-dryrun' }}
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Check the tag name
run: |
# the tag becomes a file name and the jar's version: letters, digits, dot, dash, underscore only
[[ "$TAG" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]] || { echo "::error::unsafe or empty tag name"; exit 1; }
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 (QuPath 0.7 is built with Java 25)
with: { distribution: temurin, java-version: "25" }
- name: Install xvfb and the libraries QuPath's JavaFX needs
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends xvfb xauth libgtk-3-0t64 libxtst6 libxrender1 libxi6 libxrandr2 libgl1 libasound2t64 fonts-dejavu-core
- name: Cache QuPath
id: qupath-cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: qupath-dist
key: qupath-${{ env.QUPATH_VERSION }}-${{ env.QUPATH_SHA256 }}
- name: Download and check QuPath
if: steps.qupath-cache.outputs.cache-hit != 'true'
run: |
curl -fsSL --retry 5 --retry-delay 10 -o qupath.tar.xz "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/qupath/qupath/releases/download/v${QUPATH_VERSION}/QuPath-v${QUPATH_VERSION}-Linux.tar.xz"
echo "$QUPATH_SHA256 qupath.tar.xz" | sha256sum -c -
mkdir qupath-dist
tar -xJf qupath.tar.xz -C qupath-dist
rm qupath.tar.xz
- name: Build the jar and the single-file script
run: |
test -d "$PWD/qupath-dist/QuPath/lib/app"
QUPATH="$PWD/qupath-dist/QuPath" LC_QUPATH_VERSION="$TAG" ./build.sh
test -f "target/labconstrictor-qupath-$TAG.jar"
test -s target/LabConstrictorTools-all.groovy
- name: Check the version inside the jar
run: |
unzip -p "target/labconstrictor-qupath-$TAG.jar" META-INF/MANIFEST.MF | tr -d '\r' | grep -Fx "Implementation-Version: $TAG"
- name: Smoke test the jar (QuPath on a virtual screen)
run: QUPATH="$PWD/qupath-dist/QuPath" tests/smoke_jar.sh "target/labconstrictor-qupath-$TAG.jar"
- name: Collect the release files and the notes
run: |
MIN_TOOLS=$(sed -n 's/.*MIN_TOOLS_VERSION = "\([^"]*\)".*/\1/p' src/main/resources/org/cellmigrationlab/labconstrictor/qupath/LcCompat.groovy)
[ -n "$MIN_TOOLS" ] || { echo "::error::MIN_TOOLS_VERSION not found in LcCompat.groovy"; exit 1; }
mkdir release-files
cp "target/labconstrictor-qupath-$TAG.jar" target/LabConstrictorTools-all.groovy release-files/
(cd release-files && sha256sum "labconstrictor-qupath-$TAG.jar" LabConstrictorTools-all.groovy > SHA256SUMS && cat SHA256SUMS)
{
echo "<!-- labconstrictor-qupath-build -->"
echo "### Install"
echo "Download \`labconstrictor-qupath-$TAG.jar\`, drag it onto the QuPath window (or copy it into the extensions folder shown in QuPath's Preferences) and restart QuPath. Then use **Extensions > LabConstrictor tools...**. \`LabConstrictorTools-all.groovy\` is the same script as one file for QuPath's Script Editor. \`SHA256SUMS\` lists the checksums."
echo
echo "- Built against **QuPath $QUPATH_VERSION** (Java 25). Tested: QuPath $QUPATH_VERSION on Linux; not tested on Windows or macOS."
echo "- Needs **labconstrictor-tools $MIN_TOOLS or newer** in the LabConstrictor app's environment."
} > release-files/release-notes-addition.md
cat release-files/release-notes-addition.md
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-files
path: release-files
if-no-files-found: error
retention-days: 14
publish:
# Only for a published release; a manual run is a dry run and stops after the build job.
if: github.event_name == 'release'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: release-files
path: release-files
- name: Attach the files and append the notes
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
cd release-files
gh release upload "$TAG" "labconstrictor-qupath-$TAG.jar" LabConstrictorTools-all.groovy SHA256SUMS --clobber
gh release view "$TAG" --json body --jq .body > body.md
if ! grep -q 'labconstrictor-qupath-build' body.md; then
{ cat body.md; echo; echo; cat release-notes-addition.md; } > notes.md
gh release edit "$TAG" --notes-file notes.md
fi
os-smoke:
# Windows and macOS (arm64): the same jar in the pinned QuPath build of that platform, on the runner's desktop. Best effort: these jobs inform
# (the table in the job summary says pass, fail with the reason, or "no gui"), they never block a release; publish waits for the Linux smoke only.
name: os-smoke (${{ matrix.os }})
needs: build
runs-on: ${{ matrix.os }}
continue-on-error: true
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
asset: QuPath-v0.7.0-Windows.zip
sha256: 5839743281d11eca1ac55c87ddcf92c89a30cba7ba2060f162036b0514b1e30c
- os: macos-latest # Apple silicon (arm64)
asset: QuPath-v0.7.0-Mac-arm64.pkg
sha256: 955c4cdaa239610feccd4d315a09df5bb13835407766009a56585f9504b9b2e1
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with: { python-version: "3.12" }
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: release-files
path: release-files
- name: Cache the QuPath download
id: dl-cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: qupath-download
key: qupath-dl-${{ matrix.asset }}-${{ matrix.sha256 }}
- name: Download and check QuPath
if: steps.dl-cache.outputs.cache-hit != 'true'
shell: bash
run: |
mkdir -p qupath-download
curl -fsSL --retry 5 --retry-delay 10 -o "qupath-download/${{ matrix.asset }}" "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/qupath/qupath/releases/download/v${QUPATH_VERSION}/${{ matrix.asset }}"
echo "${{ matrix.sha256 }} qupath-download/${{ matrix.asset }}" > sha.txt
if command -v sha256sum >/dev/null; then sha256sum -c sha.txt; else shasum -a 256 -c sha.txt; fi
- name: Unpack QuPath (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: Expand-Archive -Path "qupath-download/${{ matrix.asset }}" -DestinationPath qupath-dist
- name: Install QuPath (macOS)
if: runner.os == 'macOS'
run: |
sudo installer -pkg "qupath-download/${{ matrix.asset }}" -target /
sudo chown -R "$(id -un)" /Applications/QuPath*.app # the smoke puts the jar on the app's class path
- name: Register the small apps (scratch LC_HOME, this OS's Python)
shell: bash
run: PYTHON=python tests/ci_setup_os.sh "$RUNNER_TEMP/lc" main
- name: Smoke test the jar and the OS-specific code paths
shell: bash
run: |
if [ "$RUNNER_OS" = Windows ]; then
APPDIR="$PWD/qupath-dist/app"
LAUNCHER="$(find qupath-dist -maxdepth 1 -name '*console*.exe' | head -1)"
[ -n "$LAUNCHER" ] || LAUNCHER="$(find qupath-dist -maxdepth 1 -name '*.exe' | head -1)"
else
APP="$(ls -d /Applications/QuPath*.app | head -1)"
APPDIR="$APP/Contents/app"
LAUNCHER="$(find "$APP/Contents/MacOS" -maxdepth 1 -type f -name 'QuPath*' | head -1)"
fi
case "$LAUNCHER" in /*) ;; *) LAUNCHER="$PWD/$LAUNCHER";; esac
echo "QuPath app folder: $APPDIR"; echo "launcher: $LAUNCHER"; ls "$APPDIR" | head -5
LOGS="$PWD/os-logs" tests/smoke_os.sh "$APPDIR" "$LAUNCHER" "$(ls "$PWD"/release-files/labconstrictor-qupath-*.jar)" "$RUNNER_TEMP/lc/home"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ !cancelled() }}
with:
name: os-smoke-logs-${{ matrix.os }}
path: os-logs
if-no-files-found: ignore
retention-days: 14
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@ QuPath 0.7. The form is generated from the tool's declared schema ([LabConstrict
so QuPath never imports the app's packages and needs no Python of its own. Same idea as the Napari and Fiji front-ends.

## Install
1. Install the LabConstrictor app(s) (the installer registers them; check with `labconstrictor-tools list`).
2. Drop `labconstrictor-qupath-0.1.0.jar` on the QuPath window (or into the QuPath extensions folder) and restart QuPath.
3. **Extensions > LabConstrictor tools...**
1. Install a LabConstrictor app (NucleiSky, CellTracksColab, ...): its installer registers the app and its tools (check with `labconstrictor-tools list`).
2. Download `labconstrictor-qupath-<tag>.jar` from the [Releases page](https://github.com/CellMigrationLab/LabConstrictor-QuPath/releases), drag it onto the QuPath window (or copy it into the extensions folder shown in QuPath's Preferences) and restart QuPath.
3. Open **Extensions > LabConstrictor tools...**

Without the jar: the script is several files now (`src/main/resources/org/cellmigrationlab/labconstrictor/qupath/`, joined in the order of `parts.txt`; the first is the main script). `./build.sh` also writes `target/LabConstrictorTools-all.groovy`, all of them in one file: open that one in QuPath's Script Editor and run it. The main script alone no longer runs.
For scripters, a second way that needs no jar: download `LabConstrictorTools-all.groovy` from the same release (the whole script in one file), open it in QuPath's Script Editor and run it.
Every release lists the QuPath version it was built against and the minimum Tools version in its notes; `SHA256SUMS` holds the checksums of the files.

**QuPath versions and systems.** Built against QuPath **0.7.0**; QuPath 0.7 is built with Java 25 and the jar needs it, so older QuPath versions are not supported, and other 0.7.x versions are not tested.
* **Linux**: tested in CI (QuPath 0.7.0 on a virtual screen: the whole GUI test suite on every pull request, and the smoke test of the exact release jar before a release is published; a broken jar is not published).
* **macOS (Apple silicon) and Windows**: tried by the release workflow with the same jar in the QuPath 0.7.0 build of that system (menu entry, dialog, a registered app in the list, quoting, trust checks, results folders, clean-up). These runs inform but never block a release; see the job summary of the "release" workflow for the latest result. Last dry run: macOS all steps passed; Windows: the jar loads, the dialog opens and lists the app, the results-folder test passes; three of the test bodies fail there because they use POSIX tools or POSIX file permissions (the Windows trust note: the owner/mode checks of the registry do not exist on Windows, so a group-writable entry is accepted; the clean-up test needs `chmod`), so the clean-up while a results folder is in use is **not** verified on Windows. Not tested by a person on either system.

### Troubleshooting
* No entry **Extensions > LabConstrictor tools...**: the jar needs QuPath 0.7 (Help > About shows the version); restart QuPath after adding the jar and check that it is in the extensions folder.
* "No LabConstrictor app is registered on this machine": install a LabConstrictor app first, then press **Rescan apps**.
* An app is listed as skipped, or a run is refused with "... labconstrictor-tools X is older than the 0.3.0a1 this host needs: update it": update the app (its Tools must be 0.3.0a1 or newer).
* The log is `~/.labconstrictor/logs/labconstrictor.log` (or `$LC_HOME/logs/`); the status line and **Details** of the window show what failed.

Needs **labconstrictor-tools 0.3.0a1 or newer** in the app's environment (the run record and the clean-up use its helpers and its `prune` command). A run of an app with an older one is refused with `this app's labconstrictor-tools 0.2.0rc1 is older than the 0.3.0a1 this host needs: update it`.

Expand Down Expand Up @@ -108,7 +119,11 @@ Each of these is deliberate and is written once to QuPath's log (logger `labcons
* Label images are shown as a preview, not converted to QuPath annotations/detections, and tables are not added to the measurements.
* No headless/script API, no Windows/macOS testing, no menu entry per tool.

## Build and test
## Build from source, and test
Users get the jar from the Releases page (see Install). Developers: `QUPATH=/path/to/QuPath JAVA_HOME=<JDK 25> ./build.sh` writes `target/labconstrictor-qupath-0.1.0.jar` (set `LC_QUPATH_VERSION` for another version) and `target/LabConstrictorTools-all.groovy`.

**Releases.** `.github/workflows/release.yml` runs when a release is published: it builds `labconstrictor-qupath-<tag>.jar` (the tag is the version in the jar's manifest), starts that jar in QuPath 0.7.0 on a virtual screen (`tests/smoke_jar.sh`: the menu entry exists and the dialog opens), and only then attaches the jar, `LabConstrictorTools-all.groovy` and `SHA256SUMS` to the release and appends the QuPath and Tools versions to its notes. Run it by hand (Actions > release > Run workflow, with a tag name) for a dry run: same build and smoke test, files kept as workflow artifacts, no release. The same jar is then tried on Windows and macOS (`os-smoke` jobs, `tests/smoke_os.sh`, apps registered by `tests/ci_setup_os.sh`): best effort, they never block the release (`publish` waits for the Linux job only).

QUPATH=/path/to/QuPath JAVA_HOME=<JDK 25> ./build.sh # QuPath 0.7 is built with Java 25
QUPATH=/path/to/QuPath LC_HOME=<registry with NucleiSky and CellTracksColab> tests/run_gui_test.sh # needs xvfb; 21 checks

Expand Down
10 changes: 7 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#!/bin/bash
# Builds target/labconstrictor-qupath-0.1.0.jar against a QuPath installation: QUPATH=/path/to/QuPath ./build.sh
# Builds target/labconstrictor-qupath-<version>.jar against a QuPath installation: QUPATH=/path/to/QuPath ./build.sh
# The version is 0.1.0 unless LC_QUPATH_VERSION is set (the release workflow sets it to the release tag); it is the file name and the
# Implementation-Version of the jar's manifest.
set -euo pipefail
cd "$(dirname "$0")"
QUPATH="${QUPATH:?set QUPATH to the QuPath folder (the one containing lib/app)}"
VERSION="${LC_QUPATH_VERSION:-0.1.0}"
rm -rf target/classes && mkdir -p target/classes
"${JAVA_HOME:+$JAVA_HOME/bin/}javac" -cp "$QUPATH/lib/app/*" -d target/classes $(find src/main/java -name '*.java')
cp -r src/main/resources/* target/classes/
"${JAVA_HOME:+$JAVA_HOME/bin/}jar" cf target/labconstrictor-qupath-0.1.0.jar -C target/classes .
echo "built target/labconstrictor-qupath-0.1.0.jar"
printf 'Implementation-Title: LabConstrictor-QuPath\nImplementation-Version: %s\n' "$VERSION" > target/manifest.txt
"${JAVA_HOME:+$JAVA_HOME/bin/}jar" cfm "target/labconstrictor-qupath-$VERSION.jar" target/manifest.txt -C target/classes .
echo "built target/labconstrictor-qupath-$VERSION.jar"
# one file with the whole script, for QuPath's Script Editor (the jar joins the files listed in parts.txt itself)
RES=src/main/resources/org/cellmigrationlab/labconstrictor/qupath
{ while IFS= read -r part || [ -n "$part" ]; do [ -z "$part" ] || { cat "$RES/$part"; echo; }; done < "$RES/parts.txt"; } > target/LabConstrictorTools-all.groovy
Expand Down
10 changes: 5 additions & 5 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ The extension runs the tools of every installed LabConstrictor app from QuPath,
> [!WARNING]
> **Under heavy construction.** This plugin is untested and unlikely to be stable. Expect bugs, missing features and changes without notice. Do not rely on it for work you cannot redo.
>
> For QuPath 0.7. There is no ready-made file yet; the jar is built from its repository.
> For QuPath 0.7 (tested with 0.7.0 on Linux; not tested on Windows or macOS). The jar is attached to each release; until the first release is published, build it from the repository.

## What you need

- [QuPath](https://qupath.github.io) 0.7.
- At least one LabConstrictor app installed on your computer. The app's installer registers it, which is how the extension finds it.
- To build the jar: JDK 25 (QuPath 0.7 is built with Java 25), as described in the repository's README.
- Only to build the jar yourself: JDK 25 (QuPath 0.7 is built with Java 25), as described in the repository's README.

## Install

1. Get the [LabConstrictor-QuPath](https://github.com/CellMigrationLab/LabConstrictor-QuPath) repository and build `labconstrictor-qupath-0.1.0.jar` as its README describes.
2. Drop the jar onto the QuPath window, or put it in the QuPath extensions folder, and restart QuPath.
1. Download `labconstrictor-qupath-<tag>.jar` from the [Releases page](https://github.com/CellMigrationLab/LabConstrictor-QuPath/releases).
2. Drop the jar onto the QuPath window, or put it in the QuPath extensions folder (shown in QuPath's Preferences), and restart QuPath.

Without the jar: open `LabConstrictorTools.groovy` (under `src/main/resources/org/cellmigrationlab/labconstrictor/qupath/`) in QuPath's Script Editor and run it.
Without the jar: download `LabConstrictorTools-all.groovy` from the same release, open it in QuPath's Script Editor and run it.

## Open it

Expand Down
Loading
Loading