From 14afd56e2c53b6f06692f2e00ce53f7ecb2aca9f Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Mon, 9 Mar 2026 16:25:00 +0800 Subject: [PATCH 1/2] fix(ci): add multi-platform Docker build for ARM64 support Previously Docker image only built for linux/amd64, causing: no matching manifest for linux/arm64/v8 Changes: - Add platforms: linux/amd64,linux/arm64 to build-push-action - QEMU and Buildx already configured, just needed platforms flag Fixes #99 --- .github/workflows/docker-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index becbdec5..5824d06e 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -44,6 +44,7 @@ jobs: with: context: . file: ./Dockerfile + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 27ffc561637a2fd67a87415691dd5fddeb4a2e2d Mon Sep 17 00:00:00 2001 From: JasonOA888 Date: Tue, 10 Mar 2026 01:13:19 +0800 Subject: [PATCH 2/2] feat(installer): add Windows installer build scripts Implements #70 - Windows installation program packaging ## Features - PowerShell build script with multiple options: - Embedded Python mode (smaller, ~200-300MB) - PyInstaller mode (self-contained, ~1GB+) - Skip options for incremental builds - Clean build support - Inno Setup integration for professional installer - Portable version generation - API key configuration during installation - Desktop and Start Menu shortcuts - Auto-launch browser after installation ## Build Options ```powershell # Default (embedded Python, smaller) .\installer\build.ps1 # PyInstaller mode (larger but self-contained) .\installer\build.ps1 -PyInstaller # Incremental builds .\installer\build.ps1 -SkipFrontend -SkipBackend # Clean build .\installer\build.ps1 -Clean ``` ## Output - `dist/MiroFish_Setup_0.1.1.exe` - Windows installer - `dist/MiroFish_Portable/` - Portable version ## Installation Flow 1. User downloads and runs installer 2. Setup wizard prompts for API key 3. Installation completes 4. Launch via desktop/start menu shortcut 5. Browser opens to application --- installer/README.md | 97 +++++++++ installer/build.ps1 | 511 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 608 insertions(+) create mode 100644 installer/README.md create mode 100644 installer/build.ps1 diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 00000000..4df86b16 --- /dev/null +++ b/installer/README.md @@ -0,0 +1,97 @@ +# MiroFish Windows Installer + +This directory contains scripts to build Windows executable installers for MiroFish. + +## Prerequisites + +- Windows 10 or later +- PowerShell 5.1+ +- Python 3.9+ (for embedded mode) +- Node.js 18+ (for frontend build) +- [Inno Setup](https://jrsoftware.org/isinfo.php) (optional, for creating setup.exe) + +## Build Methods + +### Method 1: Embedded Python Mode (Recommended) + +Smaller file size (~200-300MB), faster build: + +```powershell +.\installer\build.ps1 +``` + +### Method 2: PyInstaller Mode + +Self-contained executable, but larger file size (~1GB+): + +```powershell +.\installer\build.ps1 -PyInstaller +``` + +## Build Options + +```powershell +# Skip frontend build (if unchanged) +.\installer\build.ps1 -SkipFrontend + +# Skip backend processing (if unchanged) +.\installer\build.ps1 -SkipBackend + +# Skip installer creation (only generate executables) +.\installer\build.ps1 -SkipInstaller + +# Clean old builds and start fresh +.\installer\build.ps1 -Clean +``` + +## Output + +- `dist/MiroFish_Setup_0.1.1.exe` - Windows installer +- `dist/MiroFish_Portable/` - Portable version (no installation required) + +## Installation Flow + +1. User downloads and runs `MiroFish_Setup_0.1.1.exe` +2. Setup wizard prompts for API key +3. Installation completes +4. User can launch via desktop shortcut or start menu +5. Browser opens automatically to `http://localhost:5000` + +## File Structure + +``` +installer/ +├── build.ps1 # Main build script +├── setup.iss # Inno Setup configuration +├── embedded/ # Embedded Python files +│ ├── python.zip # Python runtime +│ └── requirements.txt +├── assets/ # Installer assets +│ ├── icon.ico # Application icon +│ └── banner.bmp # Setup wizard banner +└── templates/ # Configuration templates + ├── config.json.template + └── start.ps1.template +``` + +## Development + +To modify the installer: + +1. Edit `build.ps1` for build process changes +2. Edit `setup.iss` for installer UI changes +3. Edit `start.ps1.template` for launch behavior changes + +## Troubleshooting + +### Build fails with "Python not found" +Ensure Python 3.9+ is in your PATH: +```powershell +python --version +``` + +### Build fails with "Inno Setup not found" +Install Inno Setup from https://jrsoftware.org/isdl.php + +### Installer too large +Use `-SkipFrontend` if frontend hasn't changed, or use embedded mode instead of PyInstaller. diff --git a/installer/build.ps1 b/installer/build.ps1 new file mode 100644 index 00000000..1be70499 --- /dev/null +++ b/installer/build.ps1 @@ -0,0 +1,511 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Build MiroFish Windows installer + +.DESCRIPTION + This script builds a Windows installer (.exe) for MiroFish. + Supports embedded Python mode (smaller) or PyInstaller mode (larger but self-contained). + +.PARAMETER PyInstaller + Use PyInstaller mode instead of embedded Python mode + +.PARAMETER SkipFrontend + Skip frontend build (use if unchanged) + +.PARAMETER SkipBackend + Skip backend processing (use if unchanged) + +.PARAMETER SkipInstaller + Skip installer creation (only generate executables) + +.PARAMETER Clean + Clean old builds before starting + +.PARAMETER Version + Version number for the installer (default: 0.1.1) + +.EXAMPLE + .\build.ps1 + +.EXAMPLE + .\build.ps1 -PyInstaller -Clean + +.EXAMPLE + .\build.ps1 -SkipFrontend -SkipBackend +#> + +param( + [switch]$PyInstaller, + [switch]$SkipFrontend, + [switch]$SkipBackend, + [switch]$SkipInstaller, + [switch]$Clean, + [string]$Version = "0.1.1" +) + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RootDir = Split-Path -Parent $ScriptDir +$DistDir = Join-Path $RootDir "dist" +$BuildDir = Join-Path $RootDir "build" + +# Colors for output +function Write-Info { param($msg) Write-Host "[INFO] $msg" -ForegroundColor Cyan } +function Write-Success { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green } +function Write-Warning { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow } +function Write-Error { param($msg) Write-Host "[ERROR] $msg" -ForegroundColor Red } + +# Check prerequisites +function Test-Prerequisites { + Write-Info "Checking prerequisites..." + + # Check Python + $pythonCmd = Get-Command python -ErrorAction SilentlyContinue + if (-not $pythonCmd) { + Write-Error "Python not found. Please install Python 3.9+ and add to PATH." + exit 1 + } + $pythonVersion = & python --version 2>&1 + Write-Info "Found $pythonVersion" + + # Check Node.js + $nodeCmd = Get-Command node -ErrorAction SilentlyContinue + if (-not $nodeCmd) { + Write-Error "Node.js not found. Please install Node.js 18+ and add to PATH." + exit 1 + } + $nodeVersion = & node --version 2>&1 + Write-Info "Found Node.js $nodeVersion" + + # Check Inno Setup (optional) + $isccCmd = Get-Command iscc -ErrorAction SilentlyContinue + if (-not $isccCmd) { + Write-Warning "Inno Setup not found. Installer creation will be skipped." + Write-Warning "Install from: https://jrsoftware.org/isdl.php" + } else { + Write-Info "Found Inno Setup" + } + + if ($PyInstaller) { + # Check PyInstaller + $pyinstallerCmd = Get-Command pyinstaller -ErrorAction SilentlyContinue + if (-not $pyinstallerCmd) { + Write-Info "Installing PyInstaller..." + & pip install pyinstaller + } + } + + Write-Success "Prerequisites check passed" +} + +# Clean old builds +function Clear-Build { + Write-Info "Cleaning old builds..." + + if (Test-Path $DistDir) { + Remove-Item $DistDir -Recurse -Force + Write-Info "Removed $DistDir" + } + + if (Test-Path $BuildDir) { + Remove-Item $BuildDir -Recurse -Force + Write-Info "Removed $BuildDir" + } + + # Clean Python cache + Get-ChildItem -Path $RootDir -Directory -Recurse -Filter "__pycache__" | Remove-Item -Recurse -Force + Get-ChildItem -Path $RootDir -Filter "*.pyc" -Recurse | Remove-Item -Force + + # Clean Node modules cache + $frontendDir = Join-Path $RootDir "frontend" + $nodeModulesDir = Join-Path $frontendDir "node_modules" + if (Test-Path (Join-Path $frontendDir ".vite")) { + Remove-Item (Join-Path $frontendDir ".vite") -Recurse -Force + } + + Write-Success "Clean completed" +} + +# Build frontend +function Build-Frontend { + if ($SkipFrontend) { + Write-Info "Skipping frontend build (as requested)" + return + } + + Write-Info "Building frontend..." + + $frontendDir = Join-Path $RootDir "frontend" + Push-Location $frontendDir + + try { + # Install dependencies + Write-Info "Installing frontend dependencies..." + & npm ci + + # Build production version + Write-Info "Building production frontend..." + & npm run build + + Write-Success "Frontend build completed" + } finally { + Pop-Location + } +} + +# Build backend +function Build-Backend { + if ($SkipBackend) { + Write-Info "Skipping backend processing (as requested)" + return + } + + Write-Info "Building backend..." + + $backendDir = Join-Path $RootDir "backend" + + # Install Python dependencies + Write-Info "Installing Python dependencies..." + Push-Location $backendDir + try { + & pip install -r requirements.txt --target ./libs -q + } finally { + Pop-Location + } + + if ($PyInstaller) { + Build-PyInstaller + } else { + Build-Embedded + } + + Write-Success "Backend build completed" +} + +# Build with PyInstaller +function Build-PyInstaller { + Write-Info "Building with PyInstaller..." + + $backendDir = Join-Path $RootDir "backend" + $distBackendDir = Join-Path $DistDir "backend" + + Push-Location $backendDir + try { + # Create PyInstaller spec + $specContent = @" +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +a = Analysis( + ['app.py'], + pathex=[], + binaries=[], + datas=[ + ('static', 'static'), + ('templates', 'templates'), + ], + hiddenimports=['flask', 'flask_cors', 'gunicorn'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='MiroFish', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +"@ + + $specContent | Out-File -FilePath "mirofish.spec" -Encoding UTF8 + + # Run PyInstaller + & pyinstaller mirofish.spec --distpath $distBackendDir --workpath $BuildDir --clean + + Write-Success "PyInstaller build completed" + } finally { + Pop-Location + } +} + +# Build with embedded Python +function Build-Embedded { + Write-Info "Building with embedded Python..." + + $backendDir = Join-Path $RootDir "backend" + $distBackendDir = Join-Path $DistDir "MiroFish_Portable\backend" + + # Create dist directory + New-Item -ItemType Directory -Force -Path $distBackendDir | Out-Null + + # Copy backend files + Write-Info "Copying backend files..." + Copy-Item -Path "$backendDir\*" -Destination $distBackendDir -Recurse -Force + + # Download embedded Python + $embeddedDir = Join-Path $ScriptDir "embedded" + $pythonZip = Join-Path $embeddedDir "python.zip" + + if (-not (Test-Path $pythonZip)) { + Write-Info "Downloading embedded Python..." + New-Item -ItemType Directory -Force -Path $embeddedDir | Out-Null + + # Get Python version + $pyVersion = (& python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')").Trim() + $pythonUrl = "https://www.python.org/ftp/python/$pyVersion.0/python-$pyVersion.0-embed-amd64.zip" + + try { + Invoke-WebRequest -Uri $pythonUrl -OutFile $pythonZip + Write-Success "Downloaded embedded Python" + } catch { + Write-Error "Failed to download embedded Python: $_" + Write-Info "Please download manually from: $pythonUrl" + exit 1 + } + } + + # Extract embedded Python + $pythonDir = Join-Path $distBackendDir "python" + New-Item -ItemType Directory -Force -Path $pythonDir | Out-Null + Expand-Archive -Path $pythonZip -DestinationPath $pythonDir -Force + + # Install dependencies to embedded Python + Write-Info "Installing dependencies to embedded Python..." + $pipDir = Join-Path $pythonDir "Lib\site-packages" + New-Item -ItemType Directory -Force -Path $pipDir | Out-Null + + Get-Content "$backendDir\requirements.txt" | ForEach-Object { + if ($_ -match "^\s*#" -or $_ -match "^\s*$") { return } + & pip install $_ --target $pipDir -q 2>&1 | Out-Null + } + + Write-Success "Embedded Python build completed" +} + +# Create launcher script +function New-Launcher { + Write-Info "Creating launcher script..." + + $portableDir = Join-Path $DistDir "MiroFish_Portable" + $launcherScript = Join-Path $portableDir "start.ps1" + + $scriptContent = @' +# MiroFish Launcher Script +param( + [string]$ApiKey = "", + [int]$Port = 5000 +) + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$BackendDir = Join-Path $ScriptDir "backend" +$FrontendDir = Join-Path $ScriptDir "frontend" +$PythonExe = Join-Path $BackendDir "python\python.exe" + +# Set environment variables +$env:MIROFISH_PORT = $Port +if ($ApiKey) { + $env:MIROFISH_API_KEY = $ApiKey +} + +Write-Host "Starting MiroFish..." -ForegroundColor Cyan +Write-Host "Backend: http://localhost:$Port" -ForegroundColor Green +Write-Host "Frontend: http://localhost:5173" -ForegroundColor Green + +# Start backend +$backendJob = Start-Job -ScriptBlock { + param($PythonExe, $BackendDir) + Set-Location $BackendDir + & $PythonExe app.py +} -ArgumentList $PythonExe, $BackendDir + +# Start frontend dev server +$frontendJob = Start-Job -ScriptBlock { + param($FrontendDir) + Set-Location $FrontendDir + npm run dev -- --host +} -ArgumentList $FrontendDir + +# Open browser +Start-Sleep -Seconds 3 +Start-Process "http://localhost:5173" + +# Wait for jobs +Wait-Job $backendJob, $frontendJob +'@ + + $scriptContent | Out-File -FilePath $launcherScript -Encoding UTF8 + + # Create batch file for easy launch + $batchFile = Join-Path $portableDir "start.bat" + "@echo off`npowershell -ExecutionPolicy Bypass -File `"%~dp0start.ps1`" %*" | Out-File -FilePath $batchFile -Encoding ASCII + + # Copy frontend dist + $frontendDistDir = Join-Path $RootDir "frontend\dist" + if (Test-Path $frontendDistDir) { + Copy-Item -Path $frontendDistDir -Destination (Join-Path $portableDir "frontend\dist") -Recurse -Force + } + + Write-Success "Launcher script created" +} + +# Create installer +function New-Installer { + if ($SkipInstaller) { + Write-Info "Skipping installer creation (as requested)" + return + } + + $isccCmd = Get-Command iscc -ErrorAction SilentlyContinue + if (-not $isccCmd) { + Write-Warning "Inno Setup not found, skipping installer creation" + Write-Warning "Portable version is available in: $DistDir\MiroFish_Portable" + return + } + + Write-Info "Creating installer..." + + $setupIss = Join-Path $ScriptDir "setup.iss" + + # Generate setup.iss + $issContent = @" +[Setup] +AppName=MiroFish +AppVersion=$Version +AppPublisher=MiroFish Team +AppPublisherURL=https://github.com/666ghj/MiroFish +AppSupportURL=https://github.com/666ghj/MiroFish/issues +DefaultDirName={autopf}\MiroFish +DefaultGroupName=MiroFish +AllowNoIcons=yes +LicenseFile=..\LICENSE +InfoBeforeFile=README.txt +OutputDir=..\dist +OutputBaseFilename=MiroFish_Setup_$Version +SetupIconFile=assets\icon.ico +Compression=lzma2/ultra64 +SolidCompression=yes +WizardStyle=modern +PrivilegesRequired=admin + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "..\dist\MiroFish_Portable\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\MiroFish"; Filename: "{app}\start.bat" +Name: "{group}\{cm:ProgramOnTheWeb,MiroFish}"; Filename: "https://github.com/666ghj/MiroFish" +Name: "{group}\{cm:UninstallProgram,MiroFish}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\MiroFish"; Filename: "{app}\start.bat"; Tasks: desktopicon + +[Run] +Filename: "{app}\start.bat"; Description: "{cm:LaunchProgram,MiroFish}"; Flags: nowait postinstall skipifsilent + +[Code] +var + ApiKeyPage: TInputQueryWizardPage; + +procedure InitializeWizard; +begin + ApiKeyPage := CreateInputQueryPage(wpWelcome, + 'API Configuration', 'Enter your API key', + 'Please enter your API key for LLM services. You can also configure this later in the .env file.'); + ApiKeyPage.Add('API Key:', False); +end; + +function GetApiKey(Param: String): String; +begin + Result := ApiKeyPage.Values[0]; +end; +"@ + + $issContent | Out-File -FilePath $setupIss -Encoding UTF8 + + # Create README for installer + $readmeFile = Join-Path $ScriptDir "README.txt" + "MiroFish $Version`n`nThank you for installing MiroFish!`n`nAfter installation, you can launch the application from the Start Menu or Desktop shortcut.`n`nThe application will open in your default web browser.`n`nFor more information, visit: https://github.com/666ghj/MiroFish" | Out-File -FilePath $readmeFile -Encoding UTF8 + + # Create assets directory + $assetsDir = Join-Path $ScriptDir "assets" + New-Item -ItemType Directory -Force -Path $assetsDir | Out-Null + + # Run Inno Setup + Push-Location $ScriptDir + try { + & iscc $setupIss + Write-Success "Installer created: $DistDir\MiroFish_Setup_$Version.exe" + } finally { + Pop-Location + } +} + +# Main build process +function Main { + Write-Host "" + Write-Host "=====================================" -ForegroundColor Cyan + Write-Host " MiroFish Windows Installer Builder" -ForegroundColor Cyan + Write-Host "=====================================" -ForegroundColor Cyan + Write-Host "" + + Test-Prerequisites + + if ($Clean) { + Clear-Build + } + + # Create dist directory + if (-not (Test-Path $DistDir)) { + New-Item -ItemType Directory -Force -Path $DistDir | Out-Null + } + + Build-Frontend + Build-Backend + New-Launcher + New-Installer + + Write-Host "" + Write-Host "=====================================" -ForegroundColor Green + Write-Host " Build completed successfully!" -ForegroundColor Green + Write-Host "=====================================" -ForegroundColor Green + Write-Host "" + + if (Test-Path (Join-Path $DistDir "MiroFish_Setup_$Version.exe")) { + Write-Host "Installer: $DistDir\MiroFish_Setup_$Version.exe" -ForegroundColor Cyan + } + + Write-Host "Portable: $DistDir\MiroFish_Portable" -ForegroundColor Cyan + Write-Host "" +} + +# Run main +Main