diff --git a/.github/workflows/deploy-dmg.yml b/.github/workflows/deploy-dmg.yml index 3e79d7b7..775ea84f 100644 --- a/.github/workflows/deploy-dmg.yml +++ b/.github/workflows/deploy-dmg.yml @@ -1,4 +1,4 @@ -name: Build macOS .dmg +name: Deploy macOS .dmg on: workflow_call: workflow_dispatch: diff --git a/.github/workflows/deploy-pyinstaller.yml b/.github/workflows/deploy-pyinstaller.yml index b476be49..59683352 100644 --- a/.github/workflows/deploy-pyinstaller.yml +++ b/.github/workflows/deploy-pyinstaller.yml @@ -3,29 +3,33 @@ name: Deploy PyInstaller Executables on: workflow_call: workflow_dispatch: + pull_request: jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - torch_variant: [cpu, cu128] + torch_variant: [cpu, cu129] include: - os: ubuntu-latest platform: linux-x64 build_script: ./INSTALL/pyinstaller/make_pyinstaller_image.sh executable_ext: "" + macos_app_flag: "" - os: macos-latest platform: macos-x64 build_script: ./INSTALL/pyinstaller/make_pyinstaller_image.sh executable_ext: "" + macos_app_flag: "--macos-app" - os: windows-latest platform: windows-x64 build_script: ./INSTALL/pyinstaller/make_pyinstaller_image.ps1 executable_ext: ".exe" + macos_app_flag: "" exclude: - os: macos-latest - torch_variant: cu128 + torch_variant: cu129 runs-on: ${{ matrix.os }} steps: @@ -37,6 +41,25 @@ jobs: with: python-version: '3.12' + - name: Free up disk space (Linux/macOS) + if: matrix.torch_variant != 'cpu' && runner.os != 'Windows' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h + shell: bash + + - name: Free up disk space (Windows) + if: matrix.torch_variant != 'cpu' && runner.os == 'Windows' + run: | + Get-PSDrive C + Remove-Item -Recurse -Force $env:TEMP\* -ErrorAction SilentlyContinue + pip cache purge + Get-PSDrive C + shell: pwsh + - name: Install tomli for version extraction (Linux/macOS) if: runner.os != 'Windows' run: python -m pip install tomli @@ -77,7 +100,8 @@ jobs: if: runner.os != 'Windows' run: | source .venv/bin/activate - ${{ matrix.build_script }} ${{ matrix.torch_variant }} + chmod +x ${{ matrix.build_script }} + ${{ matrix.build_script }} ${{ matrix.torch_variant }} ${{ matrix.macos_app_flag }} shell: bash - name: Install dependencies and build (Windows) @@ -87,39 +111,60 @@ jobs: & ${{ matrix.build_script }} ${{ matrix.torch_variant }} shell: pwsh - - name: Rename executable - run: | - mv dist/photomap${{ matrix.executable_ext }} dist/photomap-${{ matrix.torch_variant }}${{ matrix.executable_ext }} - shell: bash - if: runner.os != 'Windows' - - - name: Rename executable (Windows) - run: | - Rename-Item -Path dist\photomap${{ matrix.executable_ext }} -NewName photomap-${{ matrix.torch_variant }}${{ matrix.executable_ext }} - shell: pwsh - if: runner.os == 'Windows' - - - name: Create zip archive with version (Linux/macOS) + - name: Rename executable or directory (Linux/macOS) if: runner.os != 'Windows' run: | cd dist - ARCHIVE_NAME="photomap-${{ matrix.platform }}-${{ matrix.torch_variant }}-v${{ steps.get_version_unix.outputs.version || steps.get_version_win.outputs.version }}.zip" - zip -j "$ARCHIVE_NAME" "photomap-${{ matrix.torch_variant }}${{ matrix.executable_ext }}" - ls -la *.zip + ls -la + BASE="photomap-${{ matrix.torch_variant }}" + EXT="${{ matrix.executable_ext }}" + VERSION="${{ steps.get_version_unix.outputs.version || steps.get_version_win.outputs.version }}" + ARCHIVE_NAME="photomap-${{ matrix.platform }}-${{ matrix.torch_variant }}-v$VERSION" + if [ -f "photomap$EXT" ]; then + mv "photomap$EXT" "$ARCHIVE_NAME$EXT" + elif [ -d "photomap.app" ]; then + mv "photomap.app" "$ARCHIVE_NAME.app" + elif [ -d "photomap" ]; then + mv "photomap" "$ARCHIVE_NAME" + else + echo "Neither photomap$EXT nor photomap directory found!" + exit 1 + fi + ls -la shell: bash - - name: Create zip archive with version (Windows) + - name: Rename executable or directory (Windows) if: runner.os == 'Windows' run: | cd dist - $archiveName = "photomap-${{ matrix.platform }}-${{ matrix.torch_variant }}-v${{ steps.get_version_unix.outputs.version || steps.get_version_win.outputs.version }}.zip" - Compress-Archive -Path "photomap-${{ matrix.torch_variant }}${{ matrix.executable_ext }}" -DestinationPath $archiveName - Get-ChildItem *.zip + $base = "photomap-${{ matrix.torch_variant }}" + $ext = "${{ matrix.executable_ext }}" + $version = "${{ steps.get_version_unix.outputs.version || steps.get_version_win.outputs.version }}" + $archiveName = "photomap-${{ matrix.platform }}-${{ matrix.torch_variant }}-v$version" + if (Test-Path "photomap$ext") { + Rename-Item -Path "photomap$ext" -NewName "$archiveName$ext" + } elseif (Test-Path "photomap") { + Rename-Item -Path "photomap" -NewName "$archiveName" + } else { + Write-Error "Neither photomap$ext nor photomap directory found!" + exit 1 + } + Get-ChildItem * + shell: pwsh + + - name: Debug - List dist contents + run: ls -la dist/ + shell: bash + if: runner.os != 'Windows' + + - name: Debug - List dist contents (Windows) + run: Get-ChildItem dist/ shell: pwsh + if: runner.os == 'Windows' - name: Upload artifact uses: actions/upload-artifact@v4 with: name: photomap-${{ matrix.platform }}-${{ matrix.torch_variant }}-v${{ steps.get_version_unix.outputs.version || steps.get_version_win.outputs.version }} - path: dist/photomap-${{ matrix.platform }}-${{ matrix.torch_variant }}-v${{ steps.get_version_unix.outputs.version || steps.get_version_win.outputs.version }}.zip + path: dist/photomap-${{ matrix.platform }}-${{ matrix.torch_variant }}-v${{ steps.get_version_unix.outputs.version || steps.get_version_win.outputs.version }}* retention-days: 30 diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 941d7caa..17f78ccc 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -1,8 +1,6 @@ name: Run Pytest on: - push: - branches: [ master, main ] pull_request: branches: [ master, main ] diff --git a/INSTALL/pyinstaller/make_pyinstaller_image.ps1 b/INSTALL/pyinstaller/make_pyinstaller_image.ps1 index f3856725..2259dbba 100644 --- a/INSTALL/pyinstaller/make_pyinstaller_image.ps1 +++ b/INSTALL/pyinstaller/make_pyinstaller_image.ps1 @@ -58,6 +58,13 @@ if ($IsWindows) { $sep = ":" } +# After installing PyTorch +pip cache purge + +# Before running PyInstaller +Write-Host "Disk space before PyInstaller:" +Get-PSDrive C + # Run PyInstaller pyinstaller ` --hidden-import clip ` @@ -90,4 +97,7 @@ pyinstaller ` $pyinstallerMode ` --name photomap ` -y ` - photomap/backend/photomap_server.py \ No newline at end of file + photomap/backend/photomap_server.py + +# After PyInstaller +Remove-Item -Recurse -Force build/ -ErrorAction SilentlyContinue \ No newline at end of file diff --git a/INSTALL/pyinstaller/make_pyinstaller_image.sh b/INSTALL/pyinstaller/make_pyinstaller_image.sh index f0dcf9e4..7b14716e 100755 --- a/INSTALL/pyinstaller/make_pyinstaller_image.sh +++ b/INSTALL/pyinstaller/make_pyinstaller_image.sh @@ -5,17 +5,37 @@ set -e # Usage info usage() { - echo "Usage: $0 [cpu|cu121|cu118|cu124|cu129|...]" + echo "Usage: $0 [cpu|cu121|cu118|cu124|cu129|...] [--macos-app]" echo " cpu - Install CPU-only PyTorch (default)" echo " cuXXX - Install CUDA-enabled PyTorch (e.g., cu121 for CUDA 12.1)" + echo " --macos-app - Create macOS .app bundle (macOS only)" exit 1 } -# Parse argument, default to "cpu" if not provided +# Parse arguments TORCH_VARIANT="${1:-cpu}" +MACOS_APP=false -# Set PyInstaller mode based on torch variant -if [[ "$TORCH_VARIANT" == cpu ]]; then +# Check for --macos-app flag +for arg in "$@"; do + case $arg in + --macos-app) + MACOS_APP=true + shift + ;; + esac +done + +# Validate macOS app option +if [[ "$MACOS_APP" == true && "$(uname)" != "Darwin" ]]; then + echo "Error: --macos-app option can only be used on macOS" + exit 1 +fi + +# Set PyInstaller mode based on torch variant and platform +if [[ "$MACOS_APP" == true ]]; then + PYINSTALLER_MODE="--windowed" +elif [[ "$TORCH_VARIANT" == cpu ]]; then PYINSTALLER_MODE="--onefile" else PYINSTALLER_MODE="--onedir" @@ -36,6 +56,10 @@ case "$TORCH_VARIANT" in ;; esac +# After installing PyTorch +pip cache purge +python -c "import torch; print(f'PyTorch cache cleared')" + # Make sure build tools and hooks are up to date python -m pip install -U pip wheel setuptools python -m pip install -U pyinstaller pyinstaller-hooks-contrib @@ -49,37 +73,82 @@ pip install . echo "Installing CLIP model..." python -c "import clip; clip.load('ViT-B/32')" +# Prepare PyInstaller arguments +PYINSTALLER_ARGS=( + --hidden-import clip + --hidden-import numpy + --hidden-import torch + --hidden-import torchvision + --hidden-import photomap + --hidden-import photomap.backend + --hidden-import photomap.backend.photomap_server + --hidden-import photomap.backend.main_wrapper + --hidden-import photomap.backend.routers + --hidden-import photomap.backend.routers.album + --hidden-import photomap.backend.routers.search + --hidden-import photomap.backend.embeddings + --hidden-import photomap.backend.config + --hidden-import uvicorn + --hidden-import fastapi + --collect-all torch + --collect-all torchvision + --collect-all clip + --collect-all numpy + --collect-all sklearn + --collect-all PIL + --collect-all photomap + --add-data "$(python -c "import clip; print(clip.__path__[0])"):clip" + --add-data "$HOME/.cache/clip:clip_models" + --add-data "photomap/frontend/static:photomap/frontend/static" + --add-data "photomap/frontend/templates:photomap/frontend/templates" + --paths . + $PYINSTALLER_MODE + --argv-emulation + --name photomap + -y +) + +# Add macOS-specific options if building app bundle +if [[ "$MACOS_APP" == true ]]; then + PYINSTALLER_ARGS+=( + --osx-bundle-identifier org.4crabs.photomap + --icon photomap/frontend/static/icons/icon.icns + ) + echo "Building macOS .app bundle..." +else + echo "Building standard executable..." +fi + # Run PyInstaller -pyinstaller \ - --hidden-import clip \ - --hidden-import numpy \ - --hidden-import torch \ - --hidden-import torchvision \ - --hidden-import photomap \ - --hidden-import photomap.backend \ - --hidden-import photomap.backend.photomap_server \ - --hidden-import photomap.backend.main_wrapper \ - --hidden-import photomap.backend.routers \ - --hidden-import photomap.backend.routers.album \ - --hidden-import photomap.backend.routers.search \ - --hidden-import photomap.backend.embeddings \ - --hidden-import photomap.backend.config \ - --hidden-import uvicorn \ - --hidden-import fastapi \ - --collect-all torch \ - --collect-all torchvision \ - --collect-all clip \ - --collect-all numpy \ - --collect-all sklearn \ - --collect-all PIL \ - --collect-all photomap \ - --add-data "$(python -c "import clip; print(clip.__path__[0])"):clip" \ - --add-data "$HOME/.cache/clip:clip_models" \ - --add-data "photomap/frontend/static:photomap/frontend/static" \ - --add-data "photomap/frontend/templates:photomap/frontend/templates" \ - --paths . \ - $PYINSTALLER_MODE \ - --argv-emulation \ - --name photomap \ - -y \ - photomap/backend/photomap_server.py \ No newline at end of file +pyinstaller "${PYINSTALLER_ARGS[@]}" photomap/backend/photomap_server.py + +# Before running PyInstaller +echo "Disk space before PyInstaller:" +df -h + +# After PyInstaller +rm -rf build/ # Remove PyInstaller temp files + +# Post-process macOS .app bundle to launch in Terminal +if [[ "$MACOS_APP" == true ]]; then + APP_BUNDLE="dist/photomap.app" + MACOS_DIR="$APP_BUNDLE/Contents/MacOS" + BIN_NAME="photomap" + + # Create a launcher script + LAUNCHER="$MACOS_DIR/run_in_terminal.sh" + cat > "$LAUNCHER" <