Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 57 additions & 17 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -504,22 +504,21 @@ jobs:
$releaseVersion = $releaseVersion.Trim()
$stage = Join-Path $env:RUNNER_TEMP "OpenHCS-Windows-Smoke"
$installRoot = Join-Path $env:RUNNER_TEMP "OpenHCS Installed"
$contractPath = Join-Path $env:RUNNER_TEMP "installer_contract.json"
New-Item -ItemType Directory -Path $stage | Out-Null
Copy-Item `
packaging/installers/windows/Install-OpenHCS.ps1 `
-Destination $stage
python scripts/render_installer_contract.py `
--version $releaseVersion `
--output $contractPath
& packaging/installers/windows/Build-InstallerLauncher.ps1 `
-OutputDirectory $stage
-OutputDirectory $stage `
-ContractPath $contractPath
if ($LASTEXITCODE -ne 0) {
throw "Windows GUI installer launcher build failed."
}
python scripts/render_installer_contract.py `
--version $releaseVersion `
--output (Join-Path $stage "installer_contract.json")

$launcher = Join-Path $stage "Install-OpenHCS.exe"
$launcher = Join-Path $stage "OpenHCS-Windows-Installer.exe"
if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) {
throw "Windows GUI installer launcher was not staged."
throw "Single-file Windows GUI installer was not staged."
}
$subsystem = python -c "import struct,sys; from pathlib import Path; data=Path(sys.argv[1]).read_bytes(); pe=struct.unpack_from('<I',data,0x3c)[0]; assert data[pe:pe+4]==b'PE\0\0'; print(struct.unpack_from('<H',data,pe+24+68)[0])" $launcher
if ($subsystem.Trim() -ne "2") {
Expand All @@ -534,19 +533,32 @@ jobs:
$cancellationPath = Join-Path `
([IO.Path]::GetTempPath()) `
("openhcs-installer-cancel-{0}.marker" -f [Guid]::NewGuid().ToString("N"))
& $powerShellExecutable `
-NoProfile -ExecutionPolicy Bypass `
-File (Join-Path $stage "Install-OpenHCS.ps1") `
-Worker -InstallRoot $installRoot `
-CancellationPath $cancellationPath
if ($LASTEXITCODE -ne 0) {
throw "Windows installer failed with exit code $LASTEXITCODE."
$installerStartInfo = [Diagnostics.ProcessStartInfo]::new()
$installerStartInfo.FileName = $launcher
$installerStartInfo.UseShellExecute = $false
foreach ($argument in @(
"-Worker",
"-InstallRoot", $installRoot,
"-CancellationPath", $cancellationPath
)) {
[void]$installerStartInfo.ArgumentList.Add([string]$argument)
}
$installerProcess = [Diagnostics.Process]::Start($installerStartInfo)
try {
$installerProcess.WaitForExit()
$installerExitCode = $installerProcess.ExitCode
}
finally {
$installerProcess.Dispose()
}
if ($installerExitCode -ne 0) {
throw "Windows installer failed with exit code $installerExitCode."
}

$desktopRoot = [Environment]::GetFolderPath("DesktopDirectory")
$summaryJson = python -m scripts.smoke_installed_desktop `
--platform windows `
--contract (Join-Path $stage "installer_contract.json") `
--contract $contractPath `
--install-root $installRoot `
--desktop-root $desktopRoot
if ($LASTEXITCODE -ne 0) {
Expand Down Expand Up @@ -630,6 +642,34 @@ jobs:
"$staged_contract" \
"$installer_app"

dmg_source="$RUNNER_TEMP/OpenHCS Installer DMG"
installer_dmg="$RUNNER_TEMP/OpenHCS-macOS-Installer.dmg"
mount_point="$RUNNER_TEMP/OpenHCS Installer Mount"
mkdir -p "$dmg_source" "$mount_point"
ditto "$installer_app" "$dmg_source/OpenHCS Installer.app"
hdiutil create \
-volname "OpenHCS Installer" \
-srcfolder "$dmg_source" \
-format UDZO \
"$installer_dmg"
hdiutil verify "$installer_dmg"
hdiutil attach \
-nobrowse \
-readonly \
-mountpoint "$mount_point" \
"$installer_dmg"
mounted_dmg=true
cleanup_dmg_mount() {
if [[ "$mounted_dmg" == true ]]; then
hdiutil detach "$mount_point" || true
fi
}
trap cleanup_dmg_mount EXIT
test -d "$mount_point/OpenHCS Installer.app"
hdiutil detach "$mount_point"
mounted_dmg=false
trap - EXIT

mkdir -p "$smoke_home"
export HOME="$smoke_home"
export UV_FIND_LINKS="$GITHUB_WORKSPACE/dist"
Expand Down
49 changes: 26 additions & 23 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,23 @@ jobs:
$source = Get-Content packaging/installers/windows/Install-OpenHCS.ps1 -Raw
[void][scriptblock]::Create($source)

- name: Build release-pinned Windows installer archive
- name: Build release-pinned single-file Windows installer
shell: pwsh
run: |
$stage = Join-Path $env:RUNNER_TEMP "OpenHCS-Windows-Installer"
$releaseVersion = $env:GITHUB_REF_NAME -replace '^v', ''
New-Item -ItemType Directory -Path $stage | Out-Null
Copy-Item `
packaging/installers/windows/Install-OpenHCS.ps1 `
-Destination $stage
$contractPath = Join-Path $env:RUNNER_TEMP "installer_contract.json"
python scripts/render_installer_contract.py `
--version $releaseVersion `
--output $contractPath
& packaging/installers/windows/Build-InstallerLauncher.ps1 `
-OutputDirectory $stage
-OutputDirectory $env:GITHUB_WORKSPACE `
-ContractPath $contractPath
if ($LASTEXITCODE -ne 0) {
throw "Windows GUI installer launcher build failed."
}
python scripts/render_installer_contract.py `
--version $releaseVersion `
--output (Join-Path $stage "installer_contract.json")
$launcher = Join-Path $stage "Install-OpenHCS.exe"
$launcher = Join-Path $env:GITHUB_WORKSPACE "OpenHCS-Windows-Installer.exe"
if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) {
throw "Windows GUI installer launcher was not staged."
throw "Single-file Windows GUI installer was not staged."
}
$subsystem = python -c "import struct,sys; from pathlib import Path; data=Path(sys.argv[1]).read_bytes(); pe=struct.unpack_from('<I',data,0x3c)[0]; assert data[pe:pe+4]==b'PE\0\0'; print(struct.unpack_from('<H',data,pe+24+68)[0])" $launcher
if ($subsystem.Trim() -ne "2") {
Expand All @@ -68,14 +65,12 @@ jobs:
if ((Get-Item -LiteralPath $launcher).Length -gt 2MB) {
throw "Windows installer launcher unexpectedly embeds a large runtime."
}
Compress-Archive -Path (Join-Path $stage "*") `
-DestinationPath OpenHCS-Windows-Installer.zip

- name: Upload Windows installer archive
- name: Upload single-file Windows installer
uses: actions/upload-artifact@v4
with:
name: openhcs-windows-installer
path: OpenHCS-Windows-Installer.zip
path: OpenHCS-Windows-Installer.exe
if-no-files-found: error

build-macos-installer:
Expand All @@ -96,25 +91,33 @@ jobs:
bash -n packaging/installers/macos/install-openhcs.sh
bash -n packaging/installers/macos/build-installer.sh

- name: Build release-pinned macOS installer archive
- name: Build release-pinned macOS installer disk image
run: |
STAGE="$RUNNER_TEMP/OpenHCS-macOS-Installer"
DMG_SOURCE="$RUNNER_TEMP/OpenHCS-macOS-DMG"
mkdir -p "$STAGE"
mkdir -p "$DMG_SOURCE"
python scripts/render_installer_contract.py \
--version "${GITHUB_REF_NAME#v}" \
--output "$STAGE/installer_contract.json"
packaging/installers/macos/build-installer.sh \
"$STAGE/installer_contract.json" \
"$STAGE/OpenHCS Installer.app"
ditto -c -k --sequesterRsrc --keepParent \
ditto \
"$STAGE/OpenHCS Installer.app" \
"$GITHUB_WORKSPACE/OpenHCS-macOS-Installer.zip"

- name: Upload macOS installer archive
"$DMG_SOURCE/OpenHCS Installer.app"
hdiutil create \
-volname "OpenHCS Installer" \
-srcfolder "$DMG_SOURCE" \
-format UDZO \
"$GITHUB_WORKSPACE/OpenHCS-macOS-Installer.dmg"
hdiutil verify "$GITHUB_WORKSPACE/OpenHCS-macOS-Installer.dmg"

- name: Upload macOS installer disk image
uses: actions/upload-artifact@v4
with:
name: openhcs-macos-installer
path: OpenHCS-macOS-Installer.zip
path: OpenHCS-macOS-Installer.dmg
if-no-files-found: error

build-and-publish:
Expand Down Expand Up @@ -173,7 +176,7 @@ jobs:
twine check dist/*
twine upload dist/* --skip-existing

- name: Download desktop installer archives
- name: Download desktop installers
uses: actions/download-artifact@v4
with:
pattern: openhcs-*-installer
Expand Down
9 changes: 5 additions & 4 deletions docs/source/development/mcp_release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ the install-surface projections:

.. code-block:: bash

RELEASE_VERSION=0.6.1
RELEASE_VERSION=0.6.2
python scripts/sync_mcp_release_metadata.py
python scripts/sync_mcp_release_metadata.py --check
python scripts/sync_mcp_release_metadata.py --check --expected-version "$RELEASE_VERSION"
Expand Down Expand Up @@ -119,11 +119,12 @@ metadata against that version, asks for confirmation, and pushes one annotated
tag.

That tag starts ``.github/workflows/publish.yml``. The workflow builds and
validates the Windows and macOS installer archives first, then builds and smoke
validates the Windows and macOS installer assets first, then builds and smoke
tests the OpenHCS wheel outside the checkout. After publishing the wheel and
source distribution to PyPI, it creates one GitHub Release containing those
Python artifacts plus ``OpenHCS-Windows-Installer.zip`` and
``OpenHCS-macOS-Installer.zip``. The dependent MCP Registry job waits until the
Python artifacts plus the directly runnable
``OpenHCS-Windows-Installer.exe`` and the single-file
``OpenHCS-macOS-Installer.dmg``. The dependent MCP Registry job waits until the
exact PyPI version is downloadable, validates the generated registry metadata,
and publishes it through GitHub OIDC.

Expand Down
13 changes: 8 additions & 5 deletions docs/source/guide_for_biologists/installation_and_setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@ the local MCP server, Napari, and Fiji/Bio-Formats support. GPU libraries are
not included. PyImageJ resolves and caches the Fiji/Bio-Formats Java
distribution on first use rather than embedding a standalone ``Fiji.app``.

Download the installer archive for your operating system from the matching
`GitHub release <https://github.com/OpenHCSDev/openhcs/releases>`_, extract it,
and open the included installer. Re-running the same installer updates the
Download the installer for your operating system from the matching
`GitHub release <https://github.com/OpenHCSDev/openhcs/releases>`_. On Windows,
download and run ``OpenHCS-Windows-Installer.exe``. On macOS, open
``OpenHCS-macOS-Installer.dmg`` and then open ``OpenHCS Installer``. Neither
platform requires ZIP extraction. Re-running the same installer updates the
isolated environment. Installation details and failures are retained in the
OpenHCS user log directory shown by the installer.

The initial bootstrap installers are not code-signed or notarized. Windows
SmartScreen or macOS Gatekeeper may therefore ask you to confirm that you trust
the downloaded release. The release archive is only the bootstrap interface;
Python and OpenHCS remain managed by uv and PyPI in the dedicated environment.
the downloaded release. The downloaded installer is only the bootstrap
interface; Python and OpenHCS remain managed by uv and PyPI in the dedicated
environment.

Manual installation
-------------------
Expand Down
2 changes: 1 addition & 1 deletion openhcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from openhcs._source_dependencies import ensure_source_checkout_external_paths

__version__ = "0.6.1"
__version__ = "0.6.2"

# Configure polystore defaults for OpenHCS integration
os.environ.setdefault("POLYSTORE_METADATA_FILENAME", "openhcs_metadata.json")
Expand Down Expand Up @@ -49,7 +49,7 @@
configured_level_name = os.environ.get("OPENHCS_LOG_LEVEL", "INFO").upper()
configured_level = getattr(logging, configured_level_name, None)
if not isinstance(configured_level, int):
raise ValueError(f"Unknown OPENHCS_LOG_LEVEL: {configured_level_name!r}")

Check failure on line 52 in openhcs/__init__.py

View workflow job for this annotation

GitHub Actions / code-quality

ruff (TRY004)

openhcs/__init__.py:52:9: TRY004 Prefer `TypeError` exception for invalid type

# Only configure if no handlers exist and level is too high
if not root_logger.handlers and root_logger.level > configured_level:
Expand Down Expand Up @@ -77,17 +77,17 @@
# PhysicalPath,
#)
#
__all__ = [
# Core functions
"initialize",
"create_config",
"run_pipeline",
"stitch_images",

# Key types
"PipelineConfig",
"BackendConfig",
"MISTConfig",
"VirtualPath",
"PhysicalPath",
]

Check failure on line 93 in openhcs/__init__.py

View workflow job for this annotation

GitHub Actions / code-quality

ruff (RUF022)

openhcs/__init__.py:80:11: RUF022 `__all__` is not sorted help: Apply an isort-style sorting to `__all__`
2 changes: 1 addition & 1 deletion packaging/codex/openhcs/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openhcs",
"version": "0.6.1",
"version": "0.6.2",
"description": "Inspect, author, validate, and run OpenHCS microscopy workflows through the local OpenHCS MCP server.",
"author": {
"name": "OpenHCSDev",
Expand Down
2 changes: 1 addition & 1 deletion packaging/codex/openhcs/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"command": "uvx",
"args": [
"--from",
"openhcs[gui,mcp]==0.6.1",
"openhcs[gui,mcp]==0.6.2",
"openhcs-mcp"
]
}
Expand Down
26 changes: 14 additions & 12 deletions packaging/installers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ entry point remain authoritative.

## What users see

The release archives are intended for users who do not want to work in a
terminal. After extracting the downloaded archive, installation stays inside a
small native window:
The release assets are intended for users who do not want to work in a
terminal. No archive extraction is required: Windows users run one executable,
and macOS users open one disk image. Installation stays inside a small native
window:

- Windows presents Welcome, installation-folder, progress, and Finish pages.
The final page can launch OpenHCS immediately.
Expand All @@ -56,7 +57,7 @@ From the repository environment:
```bash
python -m pytest -q tests/installer
python scripts/render_installer_contract.py \
--version 0.6.1 \
--version 0.6.2 \
--output /tmp/openhcs-installer-contract.json
```

Expand All @@ -66,14 +67,15 @@ each adapter.
## Release assets

Tag publication renders a contract pinned to the tag version and attaches two
archives to the GitHub release:
directly usable files to the GitHub release:

- `OpenHCS-Windows-Installer.zip` contains a small GUI-subsystem
`Install-OpenHCS.exe`, its internal PowerShell worker, and the pinned
contract. Extract the archive and double-click `Install-OpenHCS.exe`.
- `OpenHCS-macOS-Installer.zip` contains a compiled `OpenHCS Installer.app`
with the bootstrap and pinned contract embedded as application resources.
Extract the archive and double-click the application.
- `OpenHCS-Windows-Installer.exe` is a small GUI-subsystem executable with its
PowerShell worker and pinned contract embedded. Double-click the downloaded
file.
- `OpenHCS-macOS-Installer.dmg` contains the compiled
`OpenHCS Installer.app`, whose bootstrap and pinned contract are embedded as
application resources. Open the downloaded disk image, then open the
application.

Pull-request CI parses the Windows PowerShell source and compiles the
GUI-subsystem launcher on Windows, and compiles the universal Swift/AppKit
Expand All @@ -85,7 +87,7 @@ MCP generates a two-channel synthetic plate, the packaged neurite preset runs
through the real execution server, Napari receives the result, MCP validates
mounted nonzero viewer payloads, and the smoke shuts down only its dynamically
allocated TCP runtime/viewer endpoints. The tag workflow repeats the source
gates before making either archive a release asset.
gates before making either file a release asset.

Users can run the same portable acceptance after installation:

Expand Down
Loading
Loading