#Requires -Version 5.1 <# .SYNOPSIS YTSage Universal Build Script - Creates MSI Installers and ZIP Distributions .DESCRIPTION This PowerShell script builds YTSage using cx_Freeze and creates: - MSI installer packages - ZIP file distributions (portable) - Both standard and FFmpeg versions .NOTES Version: Auto-detected from setup.py Location: build/windows/windows-build-universal.ps1 Usage: Run from project root directory (build-env/) IMPORTANT: This script must be executed from the project root directory! Example: .\build\windows\windows-build-universal.ps1 -Verbose #> param( [switch]$SkipVenv, [switch]$Verbose, [switch]$FFmpeg, [switch]$MSIOnly, [switch]$ZIPOnly, [string]$OutputDir = "releases", [string]$FFmpegPath = "$env:LOCALAPPDATA\ffmpeg\ffmpeg-7.1.1-full_build\bin" ) # Set error handling $ErrorActionPreference = "Stop" # Script configuration - Auto-detect version from setup.py $APP_NAME = "YTSage" $RELEASE_DATE = Get-Date -Format "yyyy-MM-dd" # Auto-detect version from setup.py function Get-VersionFromSetup { try { $setupContent = Get-Content "build\windows\setup.py" -Raw if ($setupContent -match 'version="([^"]*)"') { $version = $matches[1] Write-Host "Detected version: $version" -ForegroundColor Gray return $version } else { Write-Warning-Custom "Could not extract version from setup.py, using 1.0.0" return "1.0.0" } } catch { Write-Warning-Custom "Could not read setup.py, using 1.0.0" return "1.0.0" } } $SCRIPT_VERSION = Get-VersionFromSetup # Determine build type $BUILD_TYPE = if ($FFmpeg) { "FFmpeg" } else { "Standard" } $SETUP_FILE = if ($FFmpeg) { "build\windows\setup-ffmpeg.py" } else { "build\windows\setup.py" } $OUTPUT_NAME_BASE = if ($FFmpeg) { "$APP_NAME-v$SCRIPT_VERSION-ffmpeg" } else { "$APP_NAME-v$SCRIPT_VERSION" } # Banner Write-Host "================================================" -ForegroundColor Cyan Write-Host "YTSage Universal Build Script v$SCRIPT_VERSION" -ForegroundColor Cyan Write-Host "Build Type: $BUILD_TYPE" -ForegroundColor Yellow Write-Host "================================================" -ForegroundColor Cyan Write-Host "" Write-Host "This script will create:" -ForegroundColor Yellow if (-not $ZIPOnly) { Write-Host "- MSI installer package" -ForegroundColor White } if (-not $MSIOnly) { Write-Host "- ZIP portable distribution" -ForegroundColor White } Write-Host "- Professional packaging with metadata" -ForegroundColor White Write-Host "" function Write-Step { param($Message) Write-Host "[STEP] $Message" -ForegroundColor Green } function Write-Error-Custom { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red } function Write-Warning-Custom { param($Message) Write-Host "[WARNING] $Message" -ForegroundColor Yellow } function Test-Command { param($Command) try { & $Command --version | Out-Null return $true } catch { return $false } } function Test-FFmpegBinaries { if (-not $FFmpeg) { return $null } Write-Step "Checking FFmpeg binaries..." $ffmpegExe = Join-Path $FFmpegPath "ffmpeg.exe" $ffprobeExe = Join-Path $FFmpegPath "ffprobe.exe" $ffplayExe = Join-Path $FFmpegPath "ffplay.exe" $missingFiles = @() if (-not (Test-Path $ffmpegExe)) { $missingFiles += "ffmpeg.exe" } if (-not (Test-Path $ffprobeExe)) { $missingFiles += "ffprobe.exe" } if (-not (Test-Path $ffplayExe)) { $missingFiles += "ffplay.exe" } if ($missingFiles.Count -gt 0) { Write-Error-Custom "Missing FFmpeg binaries: $($missingFiles -join ', ')" Write-Host "Expected FFmpeg path: $FFmpegPath" -ForegroundColor Yellow throw "FFmpeg binaries not found" } Write-Host "✓ Found all FFmpeg binaries" -ForegroundColor Green return @{ ffmpeg = $ffmpegExe ffprobe = $ffprobeExe ffplay = $ffplayExe } } function New-VirtualEnvironment { Write-Step "Creating virtual environment..." try { python -m venv venv Write-Host "Virtual environment created successfully." -ForegroundColor Green } catch { Write-Error-Custom "Failed to create virtual environment: $($_.Exception.Message)" throw } } function Start-VirtualEnvironment { Write-Step "Activating virtual environment..." $venvScript = ".\venv\Scripts\Activate.ps1" if (-not (Test-Path $venvScript)) { Write-Error-Custom "Virtual environment activation script not found at: $venvScript" throw "Virtual environment not properly created" } try { & $venvScript Write-Host "Virtual environment activated successfully." -ForegroundColor Green } catch { Write-Error-Custom "Failed to activate virtual environment: $($_.Exception.Message)" throw } } function Install-Dependencies { Write-Step "Installing/Upgrading required packages..." try { Write-Host "Upgrading pip..." -ForegroundColor White python -m pip install --upgrade pip | Out-Null Write-Host "Installing requirements..." -ForegroundColor White pip install --no-cache-dir -r requirements.txt | Out-Null Write-Host "Installing cx_Freeze..." -ForegroundColor White pip install --no-cache-dir cx_Freeze | Out-Null Write-Host "Installing yt-dlp (temporary for build)..." -ForegroundColor White pip install --no-cache-dir yt-dlp | Out-Null Write-Host "All packages installed successfully." -ForegroundColor Green } catch { Write-Error-Custom "Failed to install required packages: $($_.Exception.Message)" throw } } function Remove-BuildArtifacts { Write-Step "Cleaning previous build artifacts..." $itemsToRemove = @("build", "dist", $OutputDir) foreach ($item in $itemsToRemove) { if (Test-Path $item) { Remove-Item $item -Recurse -Force Write-Host "Removed: $item" -ForegroundColor Gray } } Write-Host "Build artifacts cleaned." -ForegroundColor Green } function Build-Executable { param($FFmpegBinaries) Write-Step "Building executable with cx_Freeze..." try { if ($FFmpegBinaries) { $env:FFMPEG_PATH = $FFmpegBinaries.ffmpeg | Split-Path } # Build the executable python $SETUP_FILE build if ($LASTEXITCODE -ne 0) { throw "cx_Freeze build failed with exit code $LASTEXITCODE" } Write-Host "Executable built successfully." -ForegroundColor Green } catch { Write-Error-Custom "Failed to build executable: $($_.Exception.Message)" throw } } function Build-MSI { Write-Step "Creating MSI installer..." try { # Build MSI package python $SETUP_FILE bdist_msi if ($LASTEXITCODE -ne 0) { throw "MSI build failed with exit code $LASTEXITCODE" } Write-Host "MSI installer created successfully." -ForegroundColor Green } catch { Write-Error-Custom "Failed to build MSI: $($_.Exception.Message)" throw } } function New-ZIPDistribution { Write-Step "Creating ZIP distribution..." try { # Ensure output directory exists if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir | Out-Null } # Find the built executable directory $executableDir = if ($FFmpeg) { "dist\YTSage-FFmpeg" } else { "dist\YTSage" } if (-not (Test-Path $executableDir)) { throw "Executable directory not found: $executableDir" } # Create ZIP file $zipPath = Join-Path $OutputDir "$OUTPUT_NAME_BASE.zip" Compress-Archive -Path "$executableDir\*" -DestinationPath $zipPath -Force Write-Host "ZIP distribution created: $zipPath" -ForegroundColor Green # Get ZIP file size $zipSize = (Get-Item $zipPath).Length $zipSizeMB = [math]::Round($zipSize / 1MB, 2) Write-Host "ZIP size: $zipSizeMB MB" -ForegroundColor Gray } catch { Write-Error-Custom "Failed to create ZIP distribution: $($_.Exception.Message)" throw } } function Move-MSIFiles { Write-Step "Organizing MSI files..." try { # Ensure output directory exists if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir | Out-Null } # Find and move MSI files $msiFiles = Get-ChildItem -Path "dist" -Filter "*.msi" -Recurse foreach ($msiFile in $msiFiles) { $newName = "$OUTPUT_NAME_BASE.msi" $destination = Join-Path $OutputDir $newName Move-Item $msiFile.FullName $destination -Force Write-Host "MSI installer moved to: $destination" -ForegroundColor Green # Get MSI file size $msiSize = (Get-Item $destination).Length $msiSizeMB = [math]::Round($msiSize / 1MB, 2) Write-Host "MSI size: $msiSizeMB MB" -ForegroundColor Gray } } catch { Write-Error-Custom "Failed to organize MSI files: $($_.Exception.Message)" throw } } function New-ReleaseNotes { Write-Step "Creating release notes..." $releaseNotes = "# YTSage v$SCRIPT_VERSION Release`n`n" $releaseNotes += "**Release Date:** $RELEASE_DATE`n" $releaseNotes += "**Build Type:** $BUILD_TYPE`n`n" $releaseNotes += "## Installation Options`n`n" $releaseNotes += "### MSI Installer (Recommended)`n" $releaseNotes += "- **File:** $OUTPUT_NAME_BASE.msi`n" $releaseNotes += "- **Installation:** Double-click to install system-wide`n" $releaseNotes += "- **Updates:** Automatic update notifications`n" $releaseNotes += "- **Uninstall:** Via Windows Programs `& Features`n`n" $releaseNotes += "### Portable ZIP`n" $releaseNotes += "- **File:** $OUTPUT_NAME_BASE.zip`n" $releaseNotes += "- **Installation:** Extract and run - no installation required`n" $releaseNotes += "- **Updates:** Manual download and replacement`n" $releaseNotes += "- **Uninstall:** Simply delete the folder`n`n" $releaseNotes += "## System Requirements`n" $releaseNotes += "- Windows 10/11 64-bit`n" $releaseNotes += "- .NET Framework 4.7.2 or later`n" $releaseNotes += "- Internet connection for yt-dlp updates`n`n" $releaseNotes += "## Support`n" $releaseNotes += "For issues or questions, please visit the project repository.`n`n" $releaseNotes += "---`n" $releaseNotes += "*Built with cx_Freeze*`n" try { $releaseNotesPath = Join-Path $OutputDir "RELEASE_NOTES.md" $releaseNotes | Out-File -FilePath $releaseNotesPath -Encoding UTF8 Write-Host "Release notes created: $releaseNotesPath" -ForegroundColor Green } catch { Write-Warning-Custom "Failed to create release notes: $($_.Exception.Message)" } } function Remove-PostBuildArtifacts { Write-Step "Cleaning up post-build artifacts..." try { if (Test-Path "build") { Remove-Item "build" -Recurse -Force Write-Host "Removed build folder." -ForegroundColor Gray } Write-Host "Post-build cleanup completed." -ForegroundColor Green } catch { Write-Warning-Custom "Some cleanup operations failed: $($_.Exception.Message)" } } function Stop-VirtualEnvironment { try { deactivate Write-Host "Virtual environment deactivated." -ForegroundColor Green } catch { Write-Warning-Custom "Failed to deactivate virtual environment (this might be normal)" } } function Show-BuildSummary { Write-Host "" Write-Host "================================================" -ForegroundColor Cyan Write-Host "Build completed successfully!" -ForegroundColor Green Write-Host "================================================" -ForegroundColor Cyan Write-Host "" Write-Host "Build Type: $BUILD_TYPE" -ForegroundColor Yellow Write-Host "Version: $SCRIPT_VERSION" -ForegroundColor Yellow Write-Host "Output Directory: $OutputDir" -ForegroundColor Yellow Write-Host "" if (Test-Path $OutputDir) { Write-Host "Created files:" -ForegroundColor Green Get-ChildItem $OutputDir | ForEach-Object { $size = if ($_.PSIsContainer) { "Folder" } else { "$([math]::Round($_.Length / 1MB, 2)) MB" } Write-Host " ✓ $($_.Name) ($size)" -ForegroundColor White } } Write-Host "" Write-Host "Distribution ready for release!" -ForegroundColor Green Write-Host "" } # Main execution try { # Validation Write-Step "Checking prerequisites..." if (-not (Test-Command "python")) { Write-Error-Custom "Python is not installed or not in PATH" exit 1 } if (-not (Test-Path $SETUP_FILE)) { Write-Error-Custom "Setup file not found: $SETUP_FILE" exit 1 } $pythonVersion = python --version Write-Host "Found: $pythonVersion" -ForegroundColor Green # Check for Python 3.13 MSI limitation $pythonVersionInfo = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" $isPython313 = $pythonVersionInfo -eq "3.13" if ($isPython313 -and -not $ZIPOnly) { Write-Warning-Custom "Python 3.13 detected. MSI building is not supported yet by cx_Freeze." Write-Host "See: https://github.com/marcelotduarte/cx_Freeze/issues/2837" -ForegroundColor Yellow Write-Host "Switching to ZIP-only mode. Use Python 3.12 for MSI support." -ForegroundColor Yellow $ZIPOnly = $true $MSIOnly = $false } # Check FFmpeg if needed $ffmpegBinaries = Test-FFmpegBinaries # Virtual environment if (-not $SkipVenv) { if (-not (Test-Path "venv")) { New-VirtualEnvironment } else { Write-Step "Using existing virtual environment..." } Start-VirtualEnvironment } # Build process Install-Dependencies Remove-BuildArtifacts Build-Executable -FFmpegBinaries $ffmpegBinaries # Create distributions if (-not $ZIPOnly) { Build-MSI Move-MSIFiles } if (-not $MSIOnly) { New-ZIPDistribution } New-ReleaseNotes Remove-PostBuildArtifacts Show-BuildSummary } catch { Write-Error-Custom "Build failed: $($_.Exception.Message)" if ($Verbose) { Write-Host $_.Exception.StackTrace -ForegroundColor Red } exit 1 } finally { if (-not $SkipVenv) { Stop-VirtualEnvironment } } Write-Host "Press any key to continue..." -ForegroundColor Gray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")