diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a53a5cf1e..c6719903ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: continuous-integration on: push: @@ -11,87 +11,78 @@ defaults: shell: bash jobs: - # This workflow contains a single job called "build" - build: - name: "Python ${{ matrix.python-version }} on ${{ matrix.os }} ${{ matrix.QT_API }}" - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-20.04 - python-version: "3.10" - QT_API: PySide6 - with_opencl: false - #- os: ubuntu-22.04 - # python-version: "3.11" - # QT_API: PyQt6 - # with_opencl: true - #- os: ubuntu-22.04 - # python-version: "3.13" - # QT_API: PySide6 - # with_opencl: true + # build_windows_installer: + # name: Build the Windows installer + # runs-on: windows-2022 + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v5 + # with: + # python-version: "3.11" + # cache: "pip" + # - name: Install silx + # run: pip install .[full,test] + # - name: Install pyinstaller + # # Install PyInstaller from source and compile bootloader. + # env: + # PYINSTALLER_COMPILE_BOOTLOADER: "1" + # PYINSTALLER_BOOTLOADER_WAF_ARGS: "--msvc_target=x64" + # run: pip install pyinstaller --no-binary pyinstaller + # - name: Build the package with all dependencies + # run: | + # cd package/windows + # pyinstaller pyinstaller.spec + # - uses: actions/upload-artifact@v4 + # with: + # name: windows-installer + # path: | + # ./package/windows/artifacts/silx-*.exe + # ./package/windows/artifacts/silx-*.zip - #- os: macos-13 - # python-version: "3.12" - # QT_API: PyQt5 - # with_opencl: true - - os: macos-13 - python-version: "3.13" - QT_API: PyQt6 - with_opencl: true - #- os: macos-13 - # python-version: "3.10" - # QT_API: PySide6 - # with_opencl: true - - #- os: windows-latest - # python-version: "3.13" - # QT_API: PyQt5 - # with_opencl: false - #- os: windows-latest - # python-version: "3.10" - # QT_API: PyQt6 - # with_opencl: false - - os: windows-latest - python-version: "3.12" - QT_API: PyQt5 - with_opencl: false + # test_windows_installer: + # needs: [build_windows_installer] + # name: Test the Windows installer + # runs-on: windows-2022 + # steps: + # - uses: actions/download-artifact@v4 + # with: + # name: windows-installer + # - name: Unzip + # run: 7z x silx-*.zip + # - name: Test + # run: | + # cd silx + # ./silx-view.exe --help + # ./silx.exe --help + build_and_notarize_macos_app: + name: Build the macOS app + runs-on: macos-13 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: "3.11" cache: "pip" - - - uses: ./.github/actions/setup-system - - - name: Install build dependencies - run: | - pip install --upgrade --pre build cython setuptools wheel - pip list - - - name: Build + - name: Install silx + run: pip install .[full] + - name: Install pyinstaller + # Install PyInstaller from source and compile bootloader. env: - MACOSX_DEPLOYMENT_TARGET: "10.9" - run: | - python -m build --no-isolation - ls dist - - - name: Install - run: | - pip install -r ci/requirements-pinned.txt - pip install --pre "${{ matrix.QT_API }}" - pip install --pre "$(ls dist/silx*.whl)[full,test]" - python ./ci/info_platform.py - pip list - - - name: Test + PYINSTALLER_COMPILE_BOOTLOADER: "1" + run: pip install pyinstaller --no-binary pyinstaller + - name: Build the package with all dependencies env: - QT_API: ${{ matrix.QT_API }} - SILX_TEST_LOW_MEM: "False" - SILX_OPENCL: ${{ matrix.with_opencl && 'True' || 'False' }} + ACTIONS_STEP_DEBUG: true + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }} + CERTIFICATE_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.APPPLICATION_SPECIFIC_PASSWORD }} run: | - python -c "import silx.test, sys; sys.exit(silx.test.run_tests(verbosity=1, args=['--qt-binding=${{ matrix.QT_API }}']));" + cd package/windows + pyinstaller pyinstaller.spec + - uses: actions/upload-artifact@v4 + with: + name: macos-app diff --git a/package/desktop/silx.icns b/package/desktop/silx.icns new file mode 100644 index 0000000000..3b755b282c Binary files /dev/null and b/package/desktop/silx.icns differ diff --git a/package/windows/DS_Store b/package/windows/DS_Store new file mode 100644 index 0000000000..a44f7595f6 Binary files /dev/null and b/package/windows/DS_Store differ diff --git a/package/windows/background.pdf b/package/windows/background.pdf new file mode 100644 index 0000000000..930c38d66f Binary files /dev/null and b/package/windows/background.pdf differ diff --git a/package/windows/codesign.sh b/package/windows/codesign.sh new file mode 100755 index 0000000000..a7f9d113ef --- /dev/null +++ b/package/windows/codesign.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# The script codesigns the application bundle. + +# Exit immediately if a command exits with a non-zero status +set -e +# Print commands and their arguments as they are executed (debug mode) +set -x + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') INFO [codesign] $1" +} + +log "Setting the required environment variables." +APP_NAME="silx-view" +ROOT="${PWD}" +APP_PATH="${ROOT}/dist/${APP_NAME}.app" +KEYCHAIN_PATH="${ROOT}/notarize.keychain-db" +CERTIFICATE_PATH="${ROOT}/certificate.p12" + +log "Creating a temporary keychain." +security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" +security set-keychain-settings -lut 21600 "${KEYCHAIN_PATH}" +security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" + +log "Importing the certificate from the base64 string." +echo "${CERTIFICATE_BASE64}" | base64 --decode -o "${CERTIFICATE_PATH}" + +log "Importing the certificate to the keychain." +security import "${CERTIFICATE_PATH}" \ + -P "${CERTIFICATE_PASSWORD}" \ + -A -t cert -f pkcs12 \ + -k "${KEYCHAIN_PATH}" + +log "Configuring keychain access control for codesigning without UI prompts." +security set-key-partition-list \ + -S apple-tool:,apple: \ + -k "${KEYCHAIN_PASSWORD}" \ + "${KEYCHAIN_PATH}" + +security find-certificate ${ROOT}/notarize.keychain-db + +log "Codesigning the application bundle." +# --sign "Developer ID Application: MARIUS SEPTIMIU RETEGAN (${APPLE_TEAM_ID})" +codesign --verbose --force --deep --options=runtime \ + --entitlements ./entitlements.plist \ + --keychain "${KEYCHAIN_PATH}" \ + --timestamp "${APP_PATH}" \ + --sign \""Developer ID Application: MARIUS SEPTIMIU RETEGAN (2YU2GQDPHY)\"" + +log "Removing the certificate file and keychain." +rm "${CERTIFICATE_PATH}" +security delete-keychain "${KEYCHAIN_PATH}" + +log "Codesigning completed successfully." \ No newline at end of file diff --git a/package/windows/create-dmg.sh b/package/windows/create-dmg.sh new file mode 100755 index 0000000000..babbf5c19e --- /dev/null +++ b/package/windows/create-dmg.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# The script creates a DMG file for the application bundle. + +# Exit immediately if a command exits with a non-zero status +set -e +# Print commands and their arguments as they are executed (debug mode) +set -x + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') INFO [create-dmg] $1" +} + +log "Setting environment variables." +APP_NAME="silx-view" +ROOT="${PWD}" +APP="${ROOT}/dist/${APP_NAME}.app" +RESOURCES="${ROOT}" # The path to resources (volume icon, background, ...)." + +log "Setting derived environment variables." +ARTIFACTS="${ROOT}/artifacts" +TEMPLATE="${ARTIFACTS}/template" +TEMPLATE_DMG="${ARTIFACTS}/template.dmg" +APP_DMG="${ARTIFACTS}/${APP_NAME}.dmg" + +SLEEP_INTERVAL=5 + +log "Checking if artifacts folder exists." +[ -d "${ARTIFACTS}" ] && rm -rf "${ARTIFACTS}" + +log "Creating the artifacts folder." +mkdir -p "${ARTIFACTS}" + +log "Removing any previous images." +if [[ -e "${TEMPLATE}" ]]; then rm -rf "${TEMPLATE}"; fi +if [[ -e "${APP_DMG}" ]]; then rm -rf "${APP_DMG}"; fi + +log "Copying required files." +mkdir -p "${TEMPLATE}/.background" +cp -a "${RESOURCES}/background.pdf" "${TEMPLATE}/.background/background.pdf" +cp -a "${RESOURCES}/silx.icns" "${TEMPLATE}/.VolumeIcon.icns" +cp -a "${RESOURCES}/DS_Store" "${TEMPLATE}/.DS_Store" +cp -a "${APP}" "${TEMPLATE}/${APP_NAME}.app" +ln -s "/Applications/" "${TEMPLATE}/Applications" + +log "Creating a regular .fseventsd/no_log file." +mkdir "${TEMPLATE}/.fseventsd" +touch "${TEMPLATE}/.fseventsd/no_log" + +log "Sleeping for a few seconds." +sleep "${SLEEP_INTERVAL}" + +log "Creating the temporary disk image." +hdiutil create -verbose -format UDRW -volname "${APP_NAME}" -fs APFS \ + -srcfolder "${TEMPLATE}" \ + "${TEMPLATE_DMG}" + +log "Sleeping for a few seconds." +sleep "${SLEEP_INTERVAL}" + +log "Detaching the temporary disk image if still attached." +hdiutil detach -verbose "/Volumes/${APP_NAME}" -force || true + +log "Attaching the temporary disk image in read/write mode." +MOUNT_OUTPUT=$(hdiutil attach -readwrite -noverify -noautoopen "${TEMPLATE_DMG}" | grep '^/dev/') +DEV_NAME=$(echo -n "${MOUNT_OUTPUT}" | head -n 1 | awk '{print $1}') +MOUNT_POINT=$(echo -n "${MOUNT_OUTPUT}" | tail -n 1 | awk '{print $3}') + +log "Fixing permissions." +chmod -Rf go-w "${TEMPLATE}" || true + +log "Hiding the background directory even more." +SetFile -a V "${MOUNT_POINT}/.background" + +log "Seting the custom icon volume flag so that volume has nice icon." +SetFile -a C "${MOUNT_POINT}" + +log "Moving the icons to the appropriate position relative to the background." +WINDOW_SIZE="{0, 0, 500, 300}" +APP_ICON_POSITION="{114, 124}" +APPLICATIONS_ICON_POSITION="{386, 124}" + +osascript < + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + \ No newline at end of file diff --git a/package/windows/notarize.sh b/package/windows/notarize.sh new file mode 100755 index 0000000000..ca9fa542e3 --- /dev/null +++ b/package/windows/notarize.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# The script submits the application for notarization to Apple. + +# Exit immediately if a command exits with a non-zero status +set -e +# Print commands and their arguments as they are executed (debug mode) +set -x + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') INFO [notarize] $1" +} + +log "Setting the required environment variables." +APP_NAME="silx-view" +ROOT="${PWD}" +APP_DMG="${ROOT}/artifacts/${APP_NAME}.dmg" + +log "Submiting the application for notarization." +xcrun notarytool submit \ + --apple-id "${APPLE_ID}" \ + --team-id "${APPLE_TEAM_ID}" \ + --password "${APPLICATION_SPECIFIC_PASSWORD}" \ + --wait "${APP_DMG}" + +log "Stapling the notarization ticket to the application bundle." +xcrun stapler staple "${APP_DMG}" + +log "Notarization completed successfully." \ No newline at end of file diff --git a/package/windows/pyinstaller.spec b/package/windows/pyinstaller.spec index da368f8e42..f44522f979 100644 --- a/package/windows/pyinstaller.spec +++ b/package/windows/pyinstaller.spec @@ -7,9 +7,15 @@ import sys from PyInstaller.utils.hooks import collect_data_files, collect_submodules +from silx import strictversion -datas = [] +if sys.platform == "darwin": + icon = "silx.icns" +elif sys.platform == "win32": + icon = "silx.ico" +icon = os.path.join(os.getcwd(), icon) +datas = [] PROJECT_PATH = os.path.abspath(os.path.join(SPECPATH, "..", "..")) datas.append((os.path.join(PROJECT_PATH, "README.rst"), ".")) @@ -17,14 +23,11 @@ datas.append((os.path.join(PROJECT_PATH, "LICENSE"), ".")) datas.append((os.path.join(PROJECT_PATH, "copyright"), ".")) datas += collect_data_files("silx.resources") - hiddenimports = ["hdf5plugin"] hiddenimports += collect_submodules("fabio") - block_cipher = None - silx_a = Analysis( ["bootstrap.py"], pathex=[], @@ -38,10 +41,8 @@ silx_a = Analysis( noarchive=False, ) - silx_pyz = PYZ(silx_a.pure, silx_a.zipped_data, cipher=block_cipher) - silx_exe = EXE( silx_pyz, silx_a.scripts, @@ -54,10 +55,9 @@ silx_exe = EXE( strip=False, upx=False, console=True, - icon="silx.ico", + icon=icon, ) - silx_view_a = Analysis( ["bootstrap-silx-view.py"], pathex=[], @@ -71,10 +71,8 @@ silx_view_a = Analysis( noarchive=False, ) - silx_view_pyz = PYZ(silx_view_a.pure, silx_view_a.zipped_data, cipher=block_cipher) - silx_view_exe = EXE( silx_view_pyz, silx_view_a.scripts, @@ -87,23 +85,51 @@ silx_view_exe = EXE( strip=False, upx=False, console=False, - icon="silx.ico", + icon=icon, ) +if sys.platform == "win32": + silx_coll = COLLECT( + silx_view_exe, + silx_view_a.binaries, + silx_view_a.zipfiles, + silx_view_a.datas, + silx_exe, + silx_a.binaries, + silx_a.zipfiles, + silx_a.datas, + strip=False, + upx=False, + name="silx", + ) +elif sys.platform == "darwin": + # macOS application-bundle only for silx-view. + silx_view_coll = COLLECT( + silx_view_exe, + silx_view_a.binaries, + silx_view_a.zipfiles, + silx_view_a.datas, + strip=False, + upx=False, + name="silx-view", + ) - -silx_coll = COLLECT( - silx_view_exe, - silx_view_a.binaries, - silx_view_a.zipfiles, - silx_view_a.datas, - silx_exe, - silx_a.binaries, - silx_a.zipfiles, - silx_a.datas, - strip=False, - upx=False, - name="silx", -) + app = BUNDLE( + silx_view_coll, + name="silx-view.app", + icon=icon, + bundle_identifier="org.silx.silxview", + info_plist={ + "CFBundleIdentifier": "org.silx", + "CFBundleShortVersionString": strictversion, + "CFBundleVersion": "silx-view " + strictversion, + "LSTypeIsPackage": True, + "LSMinimumSystemVersion": "10.13.0", + "NSHumanReadableCopyright": "MIT", + "NSHighResolutionCapable": True, + "NSPrincipalClass": "NSApplication", + "NSAppleScriptEnabled": False, + }, + ) # Generate license file from current Python env @@ -134,10 +160,22 @@ It includes mainy software packages with different licenses: create_license_file("LICENSE") -# Run innosetup -def innosetup(): - from silx import strictversion +if sys.platform == "darwin": + # Codesign the application. + subprocess.call(["bash", "codesign.sh"]) + + # Pack the application in a .dmg image. + subprocess.call(["bash", "create-dmg.sh"]) + + # Submit the image for notarization. + subprocess.call(["bash", "notarize.sh"]) + # Rename the created .dmg image. + os.rename( + os.path.join("artifacts", "silx-view.dmg"), + os.path.join("artifacts", f"silx-view-{strictversion}.dmg"), + ) +elif sys.platform == "win32": config_name = "create-installer.iss" with open(config_name + ".template") as f: content = f.read().replace("#Version", strictversion) @@ -147,13 +185,10 @@ def innosetup(): subprocess.call(["iscc", os.path.join(SPECPATH, config_name)]) os.remove(config_name) - - -def make_zip(): - """Create a zip archive of the fat binary files""" - from silx import strictversion - - base_name = os.path.join(SPECPATH, "artifacts", f"silx-{strictversion}-windows-application") + # Create a zip archive of the fat binary files. + base_name = os.path.join( + SPECPATH, "artifacts", f"silx-{strictversion}-windows-application" + ) shutil.make_archive( base_name, format="zip", @@ -161,6 +196,5 @@ def make_zip(): base_dir="silx", ) - -innosetup() -make_zip() +# Remove the LICENSE file. +os.remove("LICENSE") diff --git a/package/windows/silx.icns b/package/windows/silx.icns new file mode 100644 index 0000000000..3b755b282c Binary files /dev/null and b/package/windows/silx.icns differ diff --git a/requirements-dev.txt b/requirements-dev.txt index 7e4b0530ec..4c01e202b9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,4 +20,4 @@ pytest-mock hdf5plugin # For HDF5 compression filters handling -pyinstaller>=6.0.0; sys_platform == "win32" +pyinstaller>=6.0.0; sys_platform != "linux" diff --git a/tools/svg2icns.sh b/tools/svg2icns.sh new file mode 100755 index 0000000000..54b8178292 --- /dev/null +++ b/tools/svg2icns.sh @@ -0,0 +1,90 @@ +# Script to convert SVG files to macOS .icns format. +# Adapted from https://github.com/magnusviri/svg2icns. + +#!/usr/bin/env bash + +# Exit immediately if a command fails. +set -e + +# Check if at least one argument is provided. +if [ $# -lt 1 ]; then + echo "Usage: $0 [ ...]" + echo "Converts SVG files to macOS .icns format." + exit 1 +fi + +# Check if the required command-line tools are available. +if [ ! -e "/usr/bin/sips" ]; then + echo "sips is required (are you on macOS?)." + exit 1 +fi + +if [ ! -e "/usr/bin/iconutil" ]; then + echo "iconutil is required (are you on macOS?)." + exit 1 +fi + +# Define required icon sizes for macOS iconset. +SIZES=" +16,16x16 +32,16x16@2x +64,32x32@2x +128,128x128 +256,128x128@2x +512,256x256@2x +1024,512x512@2x +" + +# Define maximum resolution for initial conversion. +MAX_SIZE=1024 + +# Process each SVG file passed as an argument. +for SVG in "$@"; do + # Check if the SVG file exists. + if [ ! -f "$SVG" ]; then + echo "Error: File '$SVG' not found." + continue + fi + + # Extract the base filename without extension. + BASE=$(basename "$SVG" | sed 's/\.[^\.]*$//') + ICONSET="$BASE.iconset" + + echo "Processing: $SVG" + + # Create the temporary iconset directory. + mkdir -p "$ICONSET" + + # Use trap to ensure cleanup on script exit or interruption. + trap 'rm -rf "$ICONSET"; echo "Cleaned up temporary directory: $ICONSET"' EXIT + + # Convert the SVG file to PNG format using sips. + sips -z $MAX_SIZE $MAX_SIZE -s format png "$SVG" --out "$ICONSET/$BASE.png" + + # Generate each icon size using sips (Scriptable Image Processing System). + for PARAMS in $SIZES; do + SIZE=$(echo $PARAMS | cut -d, -f1) # Extract pixel size. + LABEL=$(echo $PARAMS | cut -d, -f2) # Extract size label for filename. + + # Resize the image to the required dimensions. + sips -z $SIZE $SIZE "$ICONSET/$BASE.png" --out "$ICONSET/icon_$LABEL.png" + done + + # Create duplicate icons for standard sizes (required by macOS). + cp "$ICONSET/icon_16x16@2x.png" "$ICONSET/icon_32x32.png" + cp "$ICONSET/icon_128x128@2x.png" "$ICONSET/icon_256x256.png" + cp "$ICONSET/icon_256x256@2x.png" "$ICONSET/icon_512x512.png" + + # Convert the iconset directory to a macOS .icns file. + iconutil -c icns "$ICONSET" + + # Clean up the temporary iconset directory and reset trap. + rm -rf "$ICONSET" + trap - EXIT + + # Move the .icns to the directory where the SVG file is located. + mv "$BASE.icns" "$(dirname "$SVG")/$BASE.icns" + + # Print success message. + echo "Created $BASE.icns" +done