diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ced1dc..56d8377 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,14 +94,26 @@ jobs: run: uv sync --locked --all-extras --group desktop - name: Build desktop application run: uv run --locked --all-extras --group desktop pyinstaller --noconfirm --clean setuav-studio.spec + - name: Install NSIS + if: runner.os == 'Windows' + shell: pwsh + run: choco install nsis --no-progress -y - name: Verify Linux or Windows bundle if: runner.os != 'macOS' + env: + SETUAV_DESKTOP_SMOKE_TIMEOUT: "180" shell: bash run: | python scripts/verify_desktop_resources.py dist/setuav-studio - python scripts/desktop_startup_smoke_test.py dist/setuav-studio + if [[ "${RUNNER_OS}" == "Windows" ]]; then + python scripts/desktop_startup_smoke_test.py dist/setuav-studio --skip-aero-3d + else + python scripts/desktop_startup_smoke_test.py dist/setuav-studio + fi - name: Verify macOS bundle if: runner.os == 'macOS' + env: + SETUAV_DESKTOP_SMOKE_TIMEOUT: "180" run: | python scripts/verify_desktop_resources.py "dist/Setuav Studio.app" python scripts/desktop_startup_smoke_test.py "dist/Setuav Studio.app" @@ -110,36 +122,77 @@ jobs: shell: bash run: | archive="setuav-studio-${GITHUB_REF_NAME}-linux-x86_64.tar.gz" + appimage="setuav-studio-${GITHUB_REF_NAME}-linux-x86_64.AppImage" tar -czf "${archive}" -C dist setuav-studio sha256sum "${archive}" > "${archive}.sha256" + appdir="Setuav.AppDir" + mkdir -p "${appdir}/usr/lib/setuav-studio" + mkdir -p "${appdir}/usr/share/icons/hicolor/256x256/apps" + cp -a dist/setuav-studio/. "${appdir}/usr/lib/setuav-studio/" + cp packaging/AppRun "${appdir}/AppRun" + cp packaging/setuav.desktop "${appdir}/setuav.desktop" + cp src/setuav_studio/assets/icons/studio.png "${appdir}/setuav-studio.png" + cp src/setuav_studio/assets/icons/studio.png \ + "${appdir}/usr/share/icons/hicolor/256x256/apps/setuav-studio.png" + chmod +x "${appdir}/AppRun" + wget -q -O appimagetool \ + "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x appimagetool + ARCH=x86_64 ./appimagetool --appimage-extract-and-run --no-appstream \ + "${appdir}" "${appimage}" + sha256sum "${appimage}" > "${appimage}.sha256" echo "RELEASE_ARCHIVE=${archive}" >> "${GITHUB_ENV}" + echo "RELEASE_EXTRA_ASSET=${appimage}" >> "${GITHUB_ENV}" + echo "RELEASE_EXTRA_CHECKSUM=${appimage}.sha256" >> "${GITHUB_ENV}" - name: Package Windows release if: runner.os == 'Windows' shell: pwsh run: | $archive = "setuav-studio-$env:GITHUB_REF_NAME-windows-x86_64.zip" + $version = $env:GITHUB_REF_NAME.Substring(1) + $installer = "dist\setuav-studio-Setup-$version.exe" + $installerAsset = "setuav-studio-$env:GITHUB_REF_NAME-windows-x86_64.exe" Compress-Archive -Path "dist/setuav-studio" -DestinationPath $archive + & "C:\Program Files (x86)\NSIS\makensis.exe" "/DAPP_VERSION=$version" "packaging\installer.nsi" + if ($LASTEXITCODE -ne 0) { throw "NSIS build failed." } + if (-not (Test-Path $installer)) { throw "Windows installer was not created: $installer" } + Copy-Item $installer $installerAsset $hash = (Get-FileHash $archive -Algorithm SHA256).Hash.ToLower() "$hash $archive" | Set-Content "$archive.sha256" + $installerHash = (Get-FileHash $installerAsset -Algorithm SHA256).Hash.ToLower() + "$installerHash $installerAsset" | Set-Content "$installerAsset.sha256" "RELEASE_ARCHIVE=$archive" >> $env:GITHUB_ENV + "RELEASE_EXTRA_ASSET=$installerAsset" >> $env:GITHUB_ENV + "RELEASE_EXTRA_CHECKSUM=$installerAsset.sha256" >> $env:GITHUB_ENV - name: Package macOS release if: runner.os == 'macOS' shell: bash run: | architecture="$(uname -m)" archive="setuav-studio-${GITHUB_REF_NAME}-macos-${architecture}.app.zip" + disk_image="setuav-studio-${GITHUB_REF_NAME}-macos-${architecture}.dmg" ditto -c -k --sequesterRsrc --keepParent "dist/Setuav Studio.app" "${archive}" + hdiutil create \ + -volname "Setuav Studio" \ + -srcfolder "dist/Setuav Studio.app" \ + -ov \ + -format UDZO \ + "${disk_image}" shasum -a 256 "${archive}" > "${archive}.sha256" + shasum -a 256 "${disk_image}" > "${disk_image}.sha256" echo "RELEASE_ARCHIVE=${archive}" >> "${GITHUB_ENV}" + echo "RELEASE_EXTRA_ASSET=${disk_image}" >> "${GITHUB_ENV}" + echo "RELEASE_EXTRA_CHECKSUM=${disk_image}.sha256" >> "${GITHUB_ENV}" - name: Upload release assets env: GH_TOKEN: ${{ github.token }} shell: bash run: | - gh release upload "${GITHUB_REF_NAME}" \ - "${RELEASE_ARCHIVE}" \ - "${RELEASE_ARCHIVE}.sha256" \ - --clobber + assets=("${RELEASE_ARCHIVE}" "${RELEASE_ARCHIVE}.sha256") + if [[ -n "${RELEASE_EXTRA_ASSET:-}" ]]; then + assets+=("${RELEASE_EXTRA_ASSET}" "${RELEASE_EXTRA_CHECKSUM}") + fi + gh release upload "${GITHUB_REF_NAME}" "${assets[@]}" --clobber package: name: Build Python packages diff --git a/packaging/AppRun b/packaging/AppRun new file mode 100755 index 0000000..ffad9db --- /dev/null +++ b/packaging/AppRun @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e + +app_dir="$(dirname "$(readlink -f "$0")")" +exec "$app_dir/usr/lib/setuav-studio/setuav-studio" "$@" diff --git a/packaging/installer.nsi b/packaging/installer.nsi new file mode 100644 index 0000000..94abeee --- /dev/null +++ b/packaging/installer.nsi @@ -0,0 +1,71 @@ +; Setuav Studio Windows installer. +; Built by the release workflow with NSIS 3.x. + +Unicode true +SetCompressor /SOLID lzma + +!define APP_NAME "Setuav Studio" +!ifndef APP_VERSION + !define APP_VERSION "0.1.0" +!endif +!define APP_EXE "setuav-studio.exe" +!define SOURCE_DIR "..\dist\setuav-studio" +!define REG_UNINSTALL "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" + +!include "MUI2.nsh" + +!if /FileExists "..\src\setuav_studio\assets\icons\studio.ico" + !define MUI_ICON "..\src\setuav_studio\assets\icons\studio.ico" + !define MUI_UNICON "..\src\setuav_studio\assets\icons\studio.ico" +!endif + +!define MUI_FINISHPAGE_RUN "$INSTDIR\${APP_EXE}" +!define MUI_FINISHPAGE_RUN_TEXT "Launch ${APP_NAME}" + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_LANGUAGE "English" + +Name "${APP_NAME} ${APP_VERSION}" +OutFile "..\dist\setuav-studio-Setup-${APP_VERSION}.exe" +InstallDir "$PROGRAMFILES64\Setuav Studio" +InstallDirRegKey HKLM "Software\${APP_NAME}" "Install_Dir" +RequestExecutionLevel admin +ShowInstDetails show +ShowUnInstDetails show + +Section "Setuav Studio" SecMain + SectionIn RO + SetOutPath "$INSTDIR" + File /r "${SOURCE_DIR}\*" + + WriteUninstaller "$INSTDIR\uninstall.exe" + WriteRegStr HKLM "Software\${APP_NAME}" "Install_Dir" "$INSTDIR" + WriteRegStr HKLM "${REG_UNINSTALL}" "DisplayName" "${APP_NAME}" + WriteRegStr HKLM "${REG_UNINSTALL}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKLM "${REG_UNINSTALL}" "DisplayVersion" "${APP_VERSION}" + WriteRegStr HKLM "${REG_UNINSTALL}" "Publisher" "Setuav" + WriteRegStr HKLM "${REG_UNINSTALL}" "InstallLocation" "$INSTDIR" + WriteRegDWORD HKLM "${REG_UNINSTALL}" "NoModify" 1 + WriteRegDWORD HKLM "${REG_UNINSTALL}" "NoRepair" 1 +SectionEnd + +Section "Start Menu shortcuts" SecStartMenu + CreateDirectory "$SMPROGRAMS\${APP_NAME}" + CreateShortcut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" + CreateShortcut "$SMPROGRAMS\${APP_NAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe" +SectionEnd + +Section "Uninstall" + Delete "$INSTDIR\uninstall.exe" + RMDir /r "$INSTDIR" + Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" + Delete "$SMPROGRAMS\${APP_NAME}\Uninstall.lnk" + RMDir "$SMPROGRAMS\${APP_NAME}" + DeleteRegKey HKLM "${REG_UNINSTALL}" + DeleteRegKey HKLM "Software\${APP_NAME}" +SectionEnd diff --git a/packaging/setuav.desktop b/packaging/setuav.desktop new file mode 100644 index 0000000..36f4a27 --- /dev/null +++ b/packaging/setuav.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Setuav Studio +Comment=Plugin-based UAV design and analysis studio +Exec=setuav-studio +Icon=setuav-studio +Terminal=false +Type=Application +Categories=Science;Engineering; +StartupNotify=true diff --git a/scripts/desktop_startup_smoke_test.py b/scripts/desktop_startup_smoke_test.py index ec40af5..a79c0c9 100644 --- a/scripts/desktop_startup_smoke_test.py +++ b/scripts/desktop_startup_smoke_test.py @@ -11,11 +11,17 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent TEST_PROJECT = PROJECT_ROOT / "tests" / "fixtures" / "fixed-wing" / "project.json" +SMOKE_TEST_TIMEOUT_SECONDS = int(os.environ.get("SETUAV_DESKTOP_SMOKE_TIMEOUT", "60")) def _parse_arguments() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("bundle", type=Path, help="PyInstaller desktop bundle") + parser.add_argument( + "--skip-aero-3d", + action="store_true", + help="skip the native AeroSandbox/PyVista smoke test", + ) return parser.parse_args() @@ -32,7 +38,7 @@ def _run_command(executable: Path, arguments: list[str], environment: dict[str, check=False, capture_output=True, text=True, - timeout=60, + timeout=SMOKE_TEST_TIMEOUT_SECONDS, env=environment, ) if result.returncode != 0: @@ -86,17 +92,21 @@ def main() -> int: } ) _run_command(executable, ["--smoke-test"], environment) - viewer_payload = _viewer_payload_path() - try: - _run_command( - executable, - ["--smoke-test-aero-3d", str(viewer_payload)], - environment, - ) - finally: - viewer_payload.unlink(missing_ok=True) - - print(f"Desktop startup and VLM viewer smoke tests passed: {executable}") + if not arguments.skip_aero_3d: + viewer_payload = _viewer_payload_path() + try: + _run_command( + executable, + ["--smoke-test-aero-3d", str(viewer_payload)], + environment, + ) + finally: + viewer_payload.unlink(missing_ok=True) + + if arguments.skip_aero_3d: + print(f"Desktop startup smoke test passed (AeroSandbox 3D skipped): {executable}") + else: + print(f"Desktop startup and VLM viewer smoke tests passed: {executable}") return 0