Skip to content

Instantly share code, notes, and snippets.

@mahmoudimus
Created January 13, 2026 00:32
Show Gist options
  • Select an option

  • Save mahmoudimus/210e7faa2c0bcda8e95fe9528149c638 to your computer and use it in GitHub Desktop.

Select an option

Save mahmoudimus/210e7faa2c0bcda8e95fe9528149c638 to your computer and use it in GitHub Desktop.
Build Jxlpy on Windows
<#
.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