Created
January 13, 2026 00:32
-
-
Save mahmoudimus/210e7faa2c0bcda8e95fe9528149c638 to your computer and use it in GitHub Desktop.
Build Jxlpy on Windows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Automates the JPEG XL + jxlpy build/install steps on Windows for a specific Python. | |
| .EXAMPLE | |
| .\Install-Jxlpy.ps1 -PythonExe "C:\path\to\python.exe" -WorkDir "C:\temp\jxlpy-build" | |
| .NOTES | |
| - Downloads: | |
| libjxl v0.11.1 Windows static zip | |
| libjxl v0.11.1 source zip (headers) | |
| jxlpy v0.9.5 source zip | |
| - Installs libs to: <PythonDir>\PCbuild\amd64 | |
| - Installs headers to:<PythonDir>\include\jxl | |
| - Patches jxlpy setup.py and runs: <PythonExe> -m pip install . | |
| - Sanity Checks: | |
| Test-Path "C:\path\to\PCbuild\amd64\jxl.lib" | |
| Test-Path "C:\path\to\include\jxl\types.h" | |
| Test-Path "C:\path\to\include\jxl\version.h" | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory = $false)] | |
| [string]$PythonExe, | |
| [Parameter(Mandatory = $false)] | |
| [string]$WorkDir = (Join-Path $PWD "jxlpy-build"), | |
| [Parameter(Mandatory = $false)] | |
| [switch]$SkipPipInstall | |
| ) | |
| if (-not $PythonExe) { | |
| $PythonExe = (Get-Command python.exe).Source | |
| } | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan } | |
| function Write-Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow } | |
| function Write-Err($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red } | |
| function Assert-FileExists([string]$Path, [string]$Label) { | |
| if (-not (Test-Path -LiteralPath $Path)) { | |
| throw "$Label not found: $Path" | |
| } | |
| } | |
| function Ensure-Dir([string]$Path) { | |
| if (-not (Test-Path -LiteralPath $Path)) { | |
| New-Item -ItemType Directory -Path $Path | Out-Null | |
| } | |
| } | |
| function Assert-FileSha256([string]$Path, [string]$ExpectedSha256) { | |
| if (-not $ExpectedSha256) { return } # allow "no hash provided" | |
| $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() | |
| $expected = $ExpectedSha256.ToLowerInvariant().Replace(" ", "") | |
| if ($actual -ne $expected) { | |
| throw "SHA256 mismatch for $Path`nExpected: $expected`nActual: $actual" | |
| } | |
| } | |
| function Download-File([string]$Url, [string]$OutFile, [string]$ExpectedSha256) { | |
| Ensure-Dir (Split-Path -Parent $OutFile) | |
| if (Test-Path -LiteralPath $OutFile) { | |
| $size = (Get-Item -LiteralPath $OutFile).Length | |
| if ($size -gt 0) { | |
| try { | |
| Assert-FileSha256 $OutFile $ExpectedSha256 | |
| Write-Info "Using cached (SHA OK): $OutFile" | |
| return | |
| } | |
| catch { | |
| Write-Warn "Cached file failed SHA check; re-downloading: $OutFile" | |
| Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| else { | |
| Write-Warn "Cached file exists but is empty; re-downloading: $OutFile" | |
| Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| Write-Info "Downloading: $Url" | |
| try { | |
| [Net.ServicePointManager]::SecurityProtocol = | |
| [Net.ServicePointManager]::SecurityProtocol -bor | |
| [Net.SecurityProtocolType]::Tls12 | |
| } | |
| catch {} | |
| Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing | |
| Assert-FileExists $OutFile "Download output" | |
| # Verify after download | |
| Assert-FileSha256 $OutFile $ExpectedSha256 | |
| Write-Info "Verified (SHA OK): $OutFile" | |
| } | |
| function Expand-Zip([string]$ZipPath, [string]$DestDir) { | |
| Write-Info "Extracting: $ZipPath -> $DestDir" | |
| if (Test-Path -LiteralPath $DestDir) { | |
| Remove-Item -LiteralPath $DestDir -Recurse -Force -ErrorAction SilentlyContinue | |
| } | |
| New-Item -ItemType Directory -Path $DestDir -Force | Out-Null | |
| Expand-Archive -LiteralPath $ZipPath -DestinationPath $DestDir -Force | |
| } | |
| function Get-PythonPrefix([string]$PythonExePath) { | |
| $PythonPrefix = & $PythonExe -c "import sys; print(sys.prefix)" | |
| return $PythonPrefix.Trim() | |
| } | |
| function Backup-File([string]$Path) { | |
| $bak = "$Path.bak" | |
| Copy-Item -LiteralPath $Path -Destination $bak -Force | |
| Write-Info "Backed up: $Path -> $bak" | |
| } | |
| function Set-TextFile([string]$Path, [string]$Content) { | |
| # Use ASCII to keep headers simple & predictable | |
| Set-Content -LiteralPath $Path -Value $Content -Encoding Ascii | |
| } | |
| function Patch-SetupPy([string]$SetupPyPath) { | |
| Write-Info "Patching setup.py: $SetupPyPath" | |
| Backup-File $SetupPyPath | |
| $text = Get-Content -LiteralPath $SetupPyPath -Raw | |
| # 1) Insert pythonPath/dirLibs block right before "jxlpy_ext = Extension(" | |
| $needle = "jxlpy_ext = Extension(" | |
| if ($text -notmatch [regex]::Escape($needle)) { | |
| throw "Could not find '$needle' in setup.py; patching aborted." | |
| } | |
| $insertBlock = @" | |
| import os | |
| import sys | |
| pythonPath = os.path.abspath(sys.executable) | |
| venvRoot = os.path.dirname(os.path.dirname(pythonPath)) if os.path.basename(os.path.dirname(pythonPath)).lower() == "scripts" else os.path.dirname(pythonPath) | |
| dirLibs = venvRoot + r'\\PCbuild\\amd64' | |
| "@ | |
| # Only insert if not already present | |
| if ($text -notmatch "dirLibs\s*=\s*venvRoot\s*\+\s*'\\\\PCbuild\\\\amd64'") { | |
| $text = $text -replace [regex]::Escape($needle), ($insertBlock + $needle) | |
| } | |
| else { | |
| Write-Warn "setup.py already seems to contain dirLibs block; skipping insertion." | |
| } | |
| # 2) Replace extra_compile_args to exactly: extra_compile_args=['-O2', '/MT'], | |
| # Try to match "extra_compile_args=..." on one line | |
| $text = [regex]::Replace( | |
| $text, | |
| "extra_compile_args\s*=\s*\[[^\]]*\]\s*,", | |
| "extra_compile_args=['-O2', '/MT']," | |
| ) | |
| # 3) Replace extra_link_args block with the requested list. | |
| $newLinkArgs = @" | |
| extra_link_args=[ | |
| dirLibs + r"\brotlicommon.lib", | |
| dirLibs + r"\brotlidec.lib", | |
| dirLibs + r"\brotlienc.lib", | |
| dirLibs + r"\hwy.lib", | |
| dirLibs + r"\jxl.lib", | |
| dirLibs + r"\jxl_cms.lib", | |
| dirLibs + r"\jxl_extras_codec.lib", | |
| dirLibs + r"\jxl_jni.lib", | |
| dirLibs + r"\jxl_threads.lib", | |
| ], | |
| "@ | |
| $patternLinkArgs = "extra_link_args\s*=\s*\[[\s\S]*?\]\s*," | |
| if ($text -match $patternLinkArgs) { | |
| $text = [regex]::Replace($text, $patternLinkArgs, $newLinkArgs) | |
| } | |
| else { | |
| Write-Warn "Could not find an existing extra_link_args=[...] block to replace. Trying to inject into Extension(...) call." | |
| # As a fallback, add after extra_compile_args if present? Probably not needed. | |
| if ($text -match "extra_compile_args=\['-O2', '/MT'\],") { | |
| $text = $text -replace "extra_compile_args=\['-O2', '/MT'\],", ("extra_compile_args=['-O2', '/MT']," + "`r`n " + $newLinkArgs) | |
| } | |
| else { | |
| throw "Fallback injection failed; please patch extra_link_args manually." | |
| } | |
| } | |
| Set-Content -LiteralPath $SetupPyPath -Value $text -Encoding UTF8 | |
| } | |
| Assert-FileExists $PythonExe "PythonExe" | |
| $PythonDir = Get-PythonPrefix $PythonExe | |
| Write-Info "Target python.exe: $PythonExe" | |
| Write-Info "Python directory: $PythonDir" | |
| Write-Info "Work directory: $WorkDir" | |
| Ensure-Dir $WorkDir | |
| # URLs | |
| $libjxlWinUrl = "https://github.com/libjxl/libjxl/releases/download/v0.11.1/jxl-x64-windows-static.zip" | |
| $libjxlSrcUrl = "https://github.com/libjxl/libjxl/archive/refs/tags/v0.11.1.zip" | |
| $jxlpyUrl = "https://github.com/olokelo/jxlpy/archive/refs/tags/0.9.5.zip" | |
| $downloads = Join-Path $WorkDir "downloads" | |
| Ensure-Dir $downloads | |
| $libjxlWinZip = Join-Path $downloads "jxl-x64-windows-static.zip" | |
| $libjxlSrcZip = Join-Path $downloads "libjxl-0.11.1.zip" | |
| $jxlpyZip = Join-Path $downloads "jxlpy-0.9.5.zip" | |
| Download-File $libjxlWinUrl $libjxlWinZip "8F53EBCE91820C30C9FC9294F06380213C1E2E66B361718880580246B2BE008E" | |
| Download-File $libjxlSrcUrl $libjxlSrcZip "240F5D6CB725B067C7F153B3B9C3DC9621C9C96D4229DC278E2C5678944E24A2" | |
| Download-File $jxlpyUrl $jxlpyZip "DC028B25484651397C5E34D4417DD96F1BE22F9115015B9C21725DAB13205604" | |
| # Step 2: Extract windows static libs into <PythonDir>\PCbuild\amd64 | |
| $pcbuildAmd64 = Join-Path $PythonDir "PCbuild\amd64" | |
| Ensure-Dir $pcbuildAmd64 | |
| # Extract to temp | |
| $tmpLibExtract = Join-Path $WorkDir "extract-libjxl-win" | |
| Expand-Zip $libjxlWinZip $tmpLibExtract | |
| # Find where jxl.lib actually landed (zip may have nested folders) | |
| Write-Info "Searching extracted files for jxl.lib..." | |
| $foundJxlLib = Get-ChildItem -LiteralPath $tmpLibExtract -Recurse -File -Filter "jxl.lib" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if (-not $foundJxlLib) { | |
| Write-Warn "Could not find jxl.lib under $tmpLibExtract. Top-level contents:" | |
| Get-ChildItem -LiteralPath $tmpLibExtract -Force | ForEach-Object { Write-Host " - $($_.FullName)" } | |
| throw "jxl.lib not found in extracted windows static zip. The zip layout may have changed." | |
| } | |
| $libRoot = Split-Path -Parent $foundJxlLib.FullName | |
| Write-Info "Found jxl.lib at: $($foundJxlLib.FullName)" | |
| Write-Info "Copying lib bundle from: $libRoot -> $pcbuildAmd64" | |
| Get-ChildItem -LiteralPath $libRoot -Force | Copy-Item -Destination $pcbuildAmd64 -Recurse -Force | |
| $expectedLib = Join-Path $pcbuildAmd64 "jxl.lib" | |
| if (-not (Test-Path -LiteralPath $expectedLib)) { | |
| Write-Warn "Did not find expected $expectedLib. Listing files in ${pcbuildAmd64}:" | |
| Get-ChildItem -LiteralPath $pcbuildAmd64 | ForEach-Object { Write-Host " - $($_.Name)" } | |
| throw "jxl.lib not found after extraction/copy. The zip layout may have changed." | |
| } | |
| Write-Info "Found: $expectedLib" | |
| # Step 4: Copy libjxl headers into <PythonDir>\include\jxl | |
| $tmpSrcExtract = Join-Path $WorkDir "extract-libjxl-src" | |
| Expand-Zip $libjxlSrcZip $tmpSrcExtract | |
| # The zip typically creates libjxl-0.11.1\... | |
| $srcRoot = Join-Path $tmpSrcExtract "libjxl-0.11.1" | |
| if (-not (Test-Path -LiteralPath $srcRoot)) { | |
| # Try: detect single top directory | |
| $top = Get-ChildItem -LiteralPath $tmpSrcExtract -Directory | Select-Object -First 1 | |
| if ($null -ne $top) { $srcRoot = $top.FullName } | |
| } | |
| Assert-FileExists $srcRoot "libjxl source root" | |
| # Find the jxl header folder anywhere under the extracted source | |
| Write-Info "Searching extracted source for header folder: lib\include\jxl" | |
| $srcHeadersDir = Get-ChildItem -LiteralPath $tmpSrcExtract -Recurse -Directory -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -match "\\lib\\include\\jxl$" } | | |
| Select-Object -First 1 | |
| if (-not $srcHeadersDir) { | |
| throw "Could not find lib\\include\\jxl in extracted libjxl source zip." | |
| } | |
| $srcHeaders = $srcHeadersDir.FullName | |
| Write-Info "Using headers directory: $srcHeaders" | |
| Assert-FileExists $srcHeaders "libjxl headers folder" | |
| $pyIncludeJxl = Join-Path $PythonDir "Include\jxl" | |
| Ensure-Dir $pyIncludeJxl | |
| Write-Info "Copying *.h headers: $srcHeaders -> $pyIncludeJxl" | |
| Get-ChildItem -LiteralPath $srcHeaders -Force | Copy-Item -Destination $pyIncludeJxl -Recurse -Force | |
| # Step 5: Create 3 generated headers in Python include\jxl | |
| Write-Info "Writing generated headers into: $pyIncludeJxl" | |
| $jxl_export_h = @' | |
| #ifndef JXL_EXPORT_H | |
| #define JXL_EXPORT_H | |
| #define JXL_EXPORT | |
| // MSVC requires [[deprecated]] | |
| #define JXL_DEPRECATED [[deprecated]] | |
| #endif /* JXL_EXPORT_H */ | |
| '@ | |
| $jxl_threads_export_h = @' | |
| #ifndef JXL_THREADS_EXPORT_H | |
| #define JXL_THREADS_EXPORT_H | |
| #define JXL_THREADS_EXPORT | |
| #endif /* JXL_THREADS_EXPORT_H */ | |
| '@ | |
| $version_h = @' | |
| /* Copyright (c) the JPEG XL Project Authors. All rights reserved. | |
| * | |
| * Use of this source code is governed by a BSD-style | |
| * license that can be found in the LICENSE file. | |
| */ | |
| /** @addtogroup libjxl_common | |
| * @{ | |
| * @file version.h | |
| * @brief libjxl version information | |
| */ | |
| #ifndef JXL_VERSION_H_ | |
| #define JXL_VERSION_H_ | |
| #define JPEGXL_MAJOR_VERSION 0 ///< JPEG XL Major version | |
| #define JPEGXL_MINOR_VERSION 11 ///< JPEG XL Minor version | |
| #define JPEGXL_PATCH_VERSION 1 ///< JPEG XL Patch version | |
| #define JPEGXL_COMPUTE_NUMERIC_VERSION(major,minor,patch) ((major<<24) | (minor<<16) | (patch<<8) | 0) | |
| /* Numeric representation of the version */ | |
| #define JPEGXL_NUMERIC_VERSION JPEGXL_COMPUTE_NUMERIC_VERSION(JPEGXL_MAJOR_VERSION,JPEGXL_MINOR_VERSION,JPEGXL_PATCH_VERSION) | |
| #endif /* JXL_VERSION_H_ */ | |
| /** @}*/ | |
| '@ | |
| Set-TextFile (Join-Path $pyIncludeJxl "jxl_export.h") $jxl_export_h | |
| Set-TextFile (Join-Path $pyIncludeJxl "jxl_threads_export.h") $jxl_threads_export_h | |
| Set-TextFile (Join-Path $pyIncludeJxl "version.h") $version_h | |
| # Step 6-7: Download + unzip jxlpy 0.9.5 and patch setup.py | |
| $tmpJxlpyExtract = Join-Path $WorkDir "extract-jxlpy" | |
| Expand-Zip $jxlpyZip $tmpJxlpyExtract | |
| # GitHub tag zip usually expands into jxlpy-0.9.5\... | |
| $jxlpyRoot = Join-Path $tmpJxlpyExtract "jxlpy-0.9.5" | |
| if (-not (Test-Path -LiteralPath $jxlpyRoot)) { | |
| $top = Get-ChildItem -LiteralPath $tmpJxlpyExtract -Directory | Select-Object -First 1 | |
| if ($null -ne $top) { $jxlpyRoot = $top.FullName } | |
| } | |
| Assert-FileExists $jxlpyRoot "jxlpy root folder" | |
| $setupPy = Join-Path $jxlpyRoot "setup.py" | |
| Assert-FileExists $setupPy "jxlpy setup.py" | |
| Patch-SetupPy $setupPy | |
| Write-Info "Patched setup.py successfully." | |
| # Step 9: pip install . | |
| if (-not $SkipPipInstall) { | |
| Write-Info "Installing with pip using: $PythonExe" | |
| Push-Location $jxlpyRoot | |
| try { | |
| & $PythonExe -m pip install --upgrade pip setuptools wheel | |
| & $PythonExe -m pip install . --no-build-isolation | |
| } | |
| finally { | |
| Pop-Location | |
| } | |
| Write-Info "Done. If build succeeded, you can now import jxlpy." | |
| } | |
| else { | |
| Write-Warn "SkipPipInstall set; not running pip install. Patched sources are at: $jxlpyRoot" | |
| } | |
| Write-Info "All steps completed." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment