mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-28 02:46:18 +03:00
774 lines
27 KiB
Python
774 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
def cmd_cd(path: str) -> str:
|
|
return f"cd /D {path}"
|
|
|
|
|
|
def cmd_set(name: str, value: str) -> str:
|
|
return f"set {name}={value}"
|
|
|
|
|
|
def cmd_append(name: str, value: str) -> str:
|
|
op = "path " if name == "PATH" else f"set {name}="
|
|
return op + f"%{name}%;{value}"
|
|
|
|
|
|
def cmd_copy(src: str, tgt: str) -> str:
|
|
return f'copy /Y /B "{src}" "{tgt}"'
|
|
|
|
|
|
def cmd_xcopy(src: str, tgt: str) -> str:
|
|
return f'xcopy /Y /E "{src}" "{tgt}"'
|
|
|
|
|
|
def cmd_mkdir(path: str) -> str:
|
|
return f'mkdir "{path}"'
|
|
|
|
|
|
def cmd_rmdir(path: str) -> str:
|
|
return f'rmdir /S /Q "{path}"'
|
|
|
|
|
|
def cmd_nmake(
|
|
makefile: str | None = None,
|
|
target: str = "",
|
|
params: list[str] | None = None,
|
|
) -> str:
|
|
return " ".join(
|
|
[
|
|
"{nmake}",
|
|
"-nologo",
|
|
f'-f "{makefile}"' if makefile is not None else "",
|
|
f'{" ".join(params)}' if params is not None else "",
|
|
f'"{target}"',
|
|
]
|
|
)
|
|
|
|
|
|
def cmds_cmake(
|
|
target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "."
|
|
) -> list[str]:
|
|
if not isinstance(target, str):
|
|
target = " ".join(target)
|
|
|
|
return [
|
|
" ".join(
|
|
[
|
|
"{cmake}",
|
|
"-DCMAKE_BUILD_TYPE=Release",
|
|
"-DCMAKE_VERBOSE_MAKEFILE=ON",
|
|
"-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake
|
|
"-DCMAKE_C_COMPILER=cl.exe", # for Ninja
|
|
"-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja
|
|
"-DCMAKE_C_FLAGS=-nologo",
|
|
"-DCMAKE_CXX_FLAGS=-nologo",
|
|
*params,
|
|
'-G "{cmake_generator}"',
|
|
f'-B "{build_dir}"',
|
|
"-S .",
|
|
]
|
|
),
|
|
f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}',
|
|
]
|
|
|
|
|
|
def cmd_msbuild(
|
|
file: str,
|
|
configuration: str = "Release",
|
|
target: str = "Build",
|
|
plat: str = "{msbuild_arch}",
|
|
) -> str:
|
|
return " ".join(
|
|
[
|
|
"{msbuild}",
|
|
f"{file}",
|
|
f'/t:"{target}"',
|
|
f'/p:Configuration="{configuration}"',
|
|
f"/p:Platform={plat}",
|
|
"/m",
|
|
]
|
|
)
|
|
|
|
|
|
SF_PROJECTS = "https://sourceforge.net/projects"
|
|
|
|
ARCHITECTURES = {
|
|
"x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"},
|
|
"AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"},
|
|
"ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"},
|
|
}
|
|
|
|
V = {
|
|
"BROTLI": "1.1.0",
|
|
"FREETYPE": "2.13.3",
|
|
"FRIBIDI": "1.0.16",
|
|
"HARFBUZZ": "10.0.1",
|
|
"JPEGTURBO": "3.0.4",
|
|
"LCMS2": "2.16",
|
|
"LIBPNG": "1.6.44",
|
|
"LIBWEBP": "1.4.0",
|
|
"OPENJPEG": "2.5.2",
|
|
"TIFF": "4.6.0",
|
|
"XZ": "5.6.3",
|
|
"ZLIB": "1.3.1",
|
|
}
|
|
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
|
|
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
|
|
V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "")
|
|
|
|
|
|
# dependencies, listed in order of compilation
|
|
DEPS: dict[str, dict[str, Any]] = {
|
|
"libjpeg": {
|
|
"url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download",
|
|
"filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz",
|
|
"dir": f"libjpeg-turbo-{V['JPEGTURBO']}",
|
|
"license": ["README.ijg", "LICENSE.md"],
|
|
"license_pattern": (
|
|
"(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n=========="
|
|
".+(libjpeg-turbo Licenses\n======================\n\n.+)$"
|
|
),
|
|
"patch": {
|
|
r"CMakeLists.txt": {
|
|
# libjpeg-turbo does not detect MSVC x86_arm64 cross-compiler correctly
|
|
'if(MSVC_IDE AND CMAKE_GENERATOR_PLATFORM MATCHES "arm64")': "if({architecture} STREQUAL ARM64)", # noqa: E501
|
|
},
|
|
},
|
|
"build": [
|
|
*cmds_cmake(
|
|
("jpeg-static", "cjpeg-static", "djpeg-static"),
|
|
"-DENABLE_SHARED:BOOL=FALSE",
|
|
"-DWITH_JPEG8:BOOL=TRUE",
|
|
"-DWITH_CRT_DLL:BOOL=TRUE",
|
|
),
|
|
cmd_copy("jpeg-static.lib", "libjpeg.lib"),
|
|
cmd_copy("cjpeg-static.exe", "cjpeg.exe"),
|
|
cmd_copy("djpeg-static.exe", "djpeg.exe"),
|
|
],
|
|
"headers": ["j*.h"],
|
|
"libs": ["libjpeg.lib"],
|
|
"bins": ["cjpeg.exe", "djpeg.exe"],
|
|
},
|
|
"zlib": {
|
|
"url": "https://zlib.net/FILENAME",
|
|
"filename": f"zlib{V['ZLIB_DOTLESS']}.zip",
|
|
"dir": f"zlib-{V['ZLIB']}",
|
|
"license": "README",
|
|
"license_pattern": "Copyright notice:\n\n(.+)$",
|
|
"build": [
|
|
cmd_nmake(r"win32\Makefile.msc", "clean"),
|
|
cmd_nmake(r"win32\Makefile.msc", "zlib.lib"),
|
|
cmd_copy("zlib.lib", "z.lib"),
|
|
],
|
|
"headers": [r"z*.h"],
|
|
"libs": [r"*.lib"],
|
|
},
|
|
"xz": {
|
|
"url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME",
|
|
"filename": f"xz-{V['XZ']}.tar.gz",
|
|
"dir": f"xz-{V['XZ']}",
|
|
"license": "COPYING",
|
|
"build": [
|
|
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
|
|
cmd_mkdir(r"{inc_dir}\lzma"),
|
|
cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"),
|
|
],
|
|
"headers": [r"src\liblzma\api\lzma.h"],
|
|
"libs": [r"lzma.lib"],
|
|
},
|
|
"libwebp": {
|
|
"url": "http://downloads.webmproject.org/releases/webp/FILENAME",
|
|
"filename": f"libwebp-{V['LIBWEBP']}.tar.gz",
|
|
"dir": f"libwebp-{V['LIBWEBP']}",
|
|
"license": "COPYING",
|
|
"patch": {
|
|
r"src\enc\picture_csp_enc.c": {
|
|
# link against libsharpyuv.lib
|
|
'#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501
|
|
}
|
|
},
|
|
"build": [
|
|
*cmds_cmake(
|
|
"webp webpmux webpdemux",
|
|
"-DBUILD_SHARED_LIBS:BOOL=OFF",
|
|
"-DWEBP_LINK_STATIC:BOOL=OFF",
|
|
),
|
|
cmd_mkdir(r"{inc_dir}\webp"),
|
|
cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"),
|
|
],
|
|
"libs": [r"libsharpyuv.lib", r"libwebp*.lib"],
|
|
},
|
|
"libtiff": {
|
|
"url": "https://download.osgeo.org/libtiff/FILENAME",
|
|
"filename": f"tiff-{V['TIFF']}.tar.gz",
|
|
"dir": f"tiff-{V['TIFF']}",
|
|
"license": "LICENSE.md",
|
|
"patch": {
|
|
r"libtiff\tif_lzma.c": {
|
|
# link against lzma.lib
|
|
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "lzma.lib")', # noqa: E501
|
|
},
|
|
r"libtiff\tif_webp.c": {
|
|
# link against libwebp.lib
|
|
"#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501
|
|
},
|
|
r"test\CMakeLists.txt": {
|
|
"add_executable(test_write_read_tags ../placeholder.h)": "",
|
|
"target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501
|
|
"target_link_libraries(test_write_read_tags PRIVATE tiff)": "",
|
|
"list(APPEND simple_tests test_write_read_tags)": "",
|
|
},
|
|
},
|
|
"build": [
|
|
*cmds_cmake(
|
|
"tiff",
|
|
"-DBUILD_SHARED_LIBS:BOOL=OFF",
|
|
"-DWebP_LIBRARY=libwebp",
|
|
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
|
|
)
|
|
],
|
|
"headers": [r"libtiff\tiff*.h"],
|
|
"libs": [r"libtiff\*.lib"],
|
|
},
|
|
"libpng": {
|
|
"url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/"
|
|
f"lpng{V['LIBPNG_DOTLESS']}.zip/download",
|
|
"filename": f"lpng{V['LIBPNG_DOTLESS']}.zip",
|
|
"dir": f"lpng{V['LIBPNG_DOTLESS']}",
|
|
"license": "LICENSE",
|
|
"build": [
|
|
*cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"),
|
|
cmd_copy(
|
|
f"libpng{V['LIBPNG_XY']}_static.lib", f"libpng{V['LIBPNG_XY']}.lib"
|
|
),
|
|
],
|
|
"headers": [r"png*.h"],
|
|
"libs": [f"libpng{V['LIBPNG_XY']}.lib"],
|
|
},
|
|
"brotli": {
|
|
"url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz",
|
|
"filename": f"brotli-{V['BROTLI']}.tar.gz",
|
|
"dir": f"brotli-{V['BROTLI']}",
|
|
"license": "LICENSE",
|
|
"build": [
|
|
*cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"),
|
|
cmd_xcopy(r"c\include", "{inc_dir}"),
|
|
],
|
|
"libs": ["*.lib"],
|
|
},
|
|
"freetype": {
|
|
"url": "https://download.savannah.gnu.org/releases/freetype/FILENAME",
|
|
"filename": f"freetype-{V['FREETYPE']}.tar.gz",
|
|
"dir": f"freetype-{V['FREETYPE']}",
|
|
"license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
|
|
"patch": {
|
|
r"builds\windows\vc2010\freetype.vcxproj": {
|
|
# freetype setting is /MD for .dll and /MT for .lib, we need /MD
|
|
"<RuntimeLibrary>MultiThreaded</RuntimeLibrary>": "<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>", # noqa: E501
|
|
# freetype doesn't specify SDK version, MSBuild may guess incorrectly
|
|
'<PropertyGroup Label="Globals">': '<PropertyGroup Label="Globals">\n <WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>', # noqa: E501
|
|
},
|
|
r"builds\windows\vc2010\freetype.user.props": {
|
|
"<UserDefines></UserDefines>": "<UserDefines>FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI</UserDefines>", # noqa: E501
|
|
"<UserIncludeDirectories></UserIncludeDirectories>": r"<UserIncludeDirectories>{dir_harfbuzz}\src;{inc_dir}</UserIncludeDirectories>", # noqa: E501
|
|
"<UserLibraryDirectories></UserLibraryDirectories>": "<UserLibraryDirectories>{lib_dir}</UserLibraryDirectories>", # noqa: E501
|
|
"<UserDependencies></UserDependencies>": f"<UserDependencies>zlib.lib;libpng{V['LIBPNG_XY']}.lib;brotlicommon.lib;brotlidec.lib</UserDependencies>", # noqa: E501
|
|
},
|
|
r"src/autofit/afshaper.c": {
|
|
# link against harfbuzz.lib
|
|
"#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501
|
|
},
|
|
},
|
|
"build": [
|
|
cmd_rmdir("objs"),
|
|
cmd_msbuild(
|
|
r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Clean"
|
|
),
|
|
cmd_msbuild(
|
|
r"builds\windows\vc2010\freetype.vcxproj", "Release Static", "Build"
|
|
),
|
|
cmd_xcopy("include", "{inc_dir}"),
|
|
],
|
|
"libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"],
|
|
},
|
|
"lcms2": {
|
|
"url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download",
|
|
"filename": f"lcms2-{V['LCMS2']}.tar.gz",
|
|
"dir": f"lcms2-{V['LCMS2']}",
|
|
"license": "LICENSE",
|
|
"patch": {
|
|
r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
|
|
# default is /MD for x86 and /MT for x64, we need /MD always
|
|
"<RuntimeLibrary>MultiThreaded</RuntimeLibrary>": "<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>", # noqa: E501
|
|
# retarget to default toolset (selected by vcvarsall.bat)
|
|
"<PlatformToolset>v143</PlatformToolset>": "<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>", # noqa: E501
|
|
# retarget to latest (selected by vcvarsall.bat)
|
|
"<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>": "<WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>", # noqa: E501
|
|
}
|
|
},
|
|
"build": [
|
|
cmd_rmdir("Lib"),
|
|
cmd_rmdir(r"Projects\VC2022\Release"),
|
|
cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"),
|
|
cmd_msbuild(
|
|
r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild"
|
|
),
|
|
cmd_xcopy("include", "{inc_dir}"),
|
|
],
|
|
"libs": [r"Lib\MS\*.lib"],
|
|
},
|
|
"openjpeg": {
|
|
"url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz",
|
|
"filename": f"openjpeg-{V['OPENJPEG']}.tar.gz",
|
|
"dir": f"openjpeg-{V['OPENJPEG']}",
|
|
"license": "LICENSE",
|
|
"build": [
|
|
*cmds_cmake(
|
|
"openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF"
|
|
),
|
|
cmd_mkdir(rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"),
|
|
cmd_copy(r"src\lib\openjp2\*.h", rf"{{inc_dir}}\openjpeg-{V['OPENJPEG']}"),
|
|
],
|
|
"libs": [r"bin\*.lib"],
|
|
},
|
|
"libimagequant": {
|
|
# commit: Merge branch 'master' into msvc (matches 2.17.0 tag)
|
|
"url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
|
|
"filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
|
|
"dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab",
|
|
"license": "COPYRIGHT",
|
|
"patch": {
|
|
"CMakeLists.txt": {
|
|
"if(OPENMP_FOUND)": "if(false)",
|
|
"install": "#install",
|
|
# libimagequant does not detect MSVC x86_arm64 cross-compiler correctly
|
|
"if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)", # noqa: E501
|
|
}
|
|
},
|
|
"build": [
|
|
*cmds_cmake("imagequant_a"),
|
|
cmd_copy("imagequant_a.lib", "imagequant.lib"),
|
|
],
|
|
"headers": [r"*.h"],
|
|
"libs": [r"imagequant.lib"],
|
|
},
|
|
"harfbuzz": {
|
|
"url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip",
|
|
"filename": f"harfbuzz-{V['HARFBUZZ']}.zip",
|
|
"dir": f"harfbuzz-{V['HARFBUZZ']}",
|
|
"license": "COPYING",
|
|
"build": [
|
|
*cmds_cmake(
|
|
"harfbuzz",
|
|
"-DHB_HAVE_FREETYPE:BOOL=TRUE",
|
|
'-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"',
|
|
),
|
|
],
|
|
"headers": [r"src\*.h"],
|
|
"libs": [r"*.lib"],
|
|
},
|
|
"fribidi": {
|
|
"url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip",
|
|
"filename": f"fribidi-{V['FRIBIDI']}.zip",
|
|
"dir": f"fribidi-{V['FRIBIDI']}",
|
|
"license": "COPYING",
|
|
"build": [
|
|
cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"),
|
|
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
|
|
# generated tab.i files cannot be cross-compiled
|
|
" ^&^& ".join(
|
|
[
|
|
"if {architecture}==ARM64 cmd /c call {vcvarsall} x86",
|
|
*cmds_cmake("fribidi-gen", "-DARCH=x86", build_dir="build_x86"),
|
|
]
|
|
),
|
|
*cmds_cmake("fribidi", "-DARCH={architecture}"),
|
|
],
|
|
"bins": [r"*.dll"],
|
|
},
|
|
}
|
|
|
|
|
|
# based on distutils._msvccompiler from CPython 3.7.4
|
|
def find_msvs(architecture: str) -> dict[str, str] | None:
|
|
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
|
|
if not root:
|
|
print("Program Files not found")
|
|
return None
|
|
|
|
requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"]
|
|
if architecture == "ARM64":
|
|
requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"]
|
|
|
|
try:
|
|
vspath = (
|
|
subprocess.check_output(
|
|
[
|
|
os.path.join(
|
|
root, "Microsoft Visual Studio", "Installer", "vswhere.exe"
|
|
),
|
|
"-latest",
|
|
"-prerelease",
|
|
*requires,
|
|
"-property",
|
|
"installationPath",
|
|
"-products",
|
|
"*",
|
|
]
|
|
)
|
|
.decode(encoding="mbcs")
|
|
.strip()
|
|
)
|
|
except (subprocess.CalledProcessError, OSError, UnicodeDecodeError):
|
|
print("vswhere not found")
|
|
return None
|
|
|
|
if not os.path.isdir(os.path.join(vspath, "VC", "Auxiliary", "Build")):
|
|
print("Visual Studio seems to be missing C compiler")
|
|
return None
|
|
|
|
# vs2017
|
|
msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe")
|
|
if not os.path.isfile(msbuild):
|
|
# vs2019
|
|
msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe")
|
|
if not os.path.isfile(msbuild):
|
|
print("Visual Studio MSBuild not found")
|
|
return None
|
|
|
|
vcvarsall = os.path.join(vspath, "VC", "Auxiliary", "Build", "vcvarsall.bat")
|
|
if not os.path.isfile(vcvarsall):
|
|
print("Visual Studio vcvarsall not found")
|
|
return None
|
|
|
|
return {
|
|
"vs_dir": vspath,
|
|
"msbuild": f'"{msbuild}"',
|
|
"vcvarsall": f'"{vcvarsall}"',
|
|
"nmake": "nmake.exe", # nmake selected by vcvarsall
|
|
}
|
|
|
|
|
|
def download_dep(url: str, file: str) -> None:
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
ex = None
|
|
for i in range(3):
|
|
try:
|
|
print(f"Fetching {url} (attempt {i + 1})...")
|
|
content = urllib.request.urlopen(url).read()
|
|
with open(file, "wb") as f:
|
|
f.write(content)
|
|
break
|
|
except urllib.error.URLError as e:
|
|
ex = e
|
|
else:
|
|
raise RuntimeError(ex)
|
|
|
|
|
|
def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None:
|
|
import tarfile
|
|
import zipfile
|
|
|
|
depends_dir = prefs["depends_dir"]
|
|
sources_dir = prefs["src_dir"]
|
|
|
|
file = os.path.join(depends_dir, filename)
|
|
if not os.path.exists(file):
|
|
# First try our mirror
|
|
mirror_url = (
|
|
f"https://raw.githubusercontent.com/"
|
|
f"python-pillow/pillow-depends/main/{filename}"
|
|
)
|
|
try:
|
|
download_dep(mirror_url, file)
|
|
except RuntimeError as exc:
|
|
# Otherwise try upstream
|
|
print(exc)
|
|
download_dep(url.replace("FILENAME", filename), file)
|
|
|
|
print("Extracting " + filename)
|
|
sources_dir_abs = os.path.abspath(sources_dir)
|
|
if filename.endswith(".zip"):
|
|
with zipfile.ZipFile(file) as zf:
|
|
for member in zf.namelist():
|
|
member_abspath = os.path.abspath(os.path.join(sources_dir, member))
|
|
member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
|
|
if sources_dir_abs != member_prefix:
|
|
msg = "Attempted Path Traversal in Zip File"
|
|
raise RuntimeError(msg)
|
|
zf.extractall(sources_dir)
|
|
elif filename.endswith((".tar.gz", ".tgz")):
|
|
with tarfile.open(file, "r:gz") as tgz:
|
|
for member in tgz.getnames():
|
|
member_abspath = os.path.abspath(os.path.join(sources_dir, member))
|
|
member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
|
|
if sources_dir_abs != member_prefix:
|
|
msg = "Attempted Path Traversal in Tar File"
|
|
raise RuntimeError(msg)
|
|
tgz.extractall(sources_dir)
|
|
else:
|
|
msg = "Unknown archive type: " + filename
|
|
raise RuntimeError(msg)
|
|
|
|
|
|
def write_script(
|
|
name: str, lines: list[str], prefs: dict[str, str], verbose: bool
|
|
) -> None:
|
|
name = os.path.join(prefs["build_dir"], name)
|
|
lines = [line.format(**prefs) for line in lines]
|
|
print("Writing " + name)
|
|
with open(name, "w", newline="") as f:
|
|
f.write(os.linesep.join(lines))
|
|
if verbose:
|
|
for line in lines:
|
|
print(" " + line)
|
|
|
|
|
|
def get_footer(dep: dict[str, Any]) -> list[str]:
|
|
lines = []
|
|
for out in dep.get("headers", []):
|
|
lines.append(cmd_copy(out, "{inc_dir}"))
|
|
for out in dep.get("libs", []):
|
|
lines.append(cmd_copy(out, "{lib_dir}"))
|
|
for out in dep.get("bins", []):
|
|
lines.append(cmd_copy(out, "{bin_dir}"))
|
|
return lines
|
|
|
|
|
|
def build_env(prefs: dict[str, str], verbose: bool) -> None:
|
|
lines = [
|
|
"if defined DISTUTILS_USE_SDK goto end",
|
|
cmd_set("INCLUDE", "{inc_dir}"),
|
|
cmd_set("INCLIB", "{lib_dir}"),
|
|
cmd_set("LIB", "{lib_dir}"),
|
|
cmd_append("PATH", "{bin_dir}"),
|
|
"call {vcvarsall} {vcvars_arch}",
|
|
cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow
|
|
cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT
|
|
":end",
|
|
"@echo on",
|
|
]
|
|
write_script("build_env.cmd", lines, prefs, verbose)
|
|
|
|
|
|
def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str:
|
|
dep = DEPS[name]
|
|
directory = dep["dir"]
|
|
file = f"build_dep_{name}.cmd"
|
|
license_dir = prefs["license_dir"]
|
|
sources_dir = prefs["src_dir"]
|
|
|
|
extract_dep(dep["url"], dep["filename"], prefs)
|
|
|
|
licenses = dep["license"]
|
|
if isinstance(licenses, str):
|
|
licenses = [licenses]
|
|
license_text = ""
|
|
for license_file in licenses:
|
|
with open(os.path.join(sources_dir, directory, license_file)) as f:
|
|
license_text += f.read()
|
|
if "license_pattern" in dep:
|
|
match = re.search(dep["license_pattern"], license_text, re.DOTALL)
|
|
assert match is not None
|
|
license_text = "\n".join(match.groups())
|
|
assert len(license_text) > 50
|
|
with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f:
|
|
print(f"Writing license {directory}.txt")
|
|
f.write(license_text)
|
|
|
|
for patch_file, patch_list in dep.get("patch", {}).items():
|
|
patch_file = os.path.join(sources_dir, directory, patch_file.format(**prefs))
|
|
with open(patch_file) as f:
|
|
text = f.read()
|
|
for patch_from, patch_to in patch_list.items():
|
|
patch_from = patch_from.format(**prefs)
|
|
patch_to = patch_to.format(**prefs)
|
|
assert patch_from in text
|
|
text = text.replace(patch_from, patch_to)
|
|
with open(patch_file, "w") as f:
|
|
print(f"Patching {patch_file}")
|
|
f.write(text)
|
|
|
|
banner = f"Building {name} ({directory})"
|
|
lines = [
|
|
r'call "{build_dir}\build_env.cmd"',
|
|
"@echo " + ("=" * 70),
|
|
f"@echo ==== {banner:<60} ====",
|
|
"@echo " + ("=" * 70),
|
|
cmd_cd(os.path.join(sources_dir, directory)),
|
|
*dep.get("build", []),
|
|
*get_footer(dep),
|
|
]
|
|
|
|
write_script(file, lines, prefs, verbose)
|
|
return file
|
|
|
|
|
|
def build_dep_all(disabled: list[str], prefs: dict[str, str], verbose: bool) -> None:
|
|
lines = [r'call "{build_dir}\build_env.cmd"']
|
|
gha_groups = "GITHUB_ACTIONS" in os.environ
|
|
for dep_name in DEPS:
|
|
print()
|
|
if dep_name in disabled:
|
|
print(f"Skipping disabled dependency {dep_name}")
|
|
continue
|
|
script = build_dep(dep_name, prefs, verbose)
|
|
if gha_groups:
|
|
lines.append(f"@echo ::group::Running {script}")
|
|
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
|
|
lines.append("if errorlevel 1 echo Build failed! && exit /B 1")
|
|
if gha_groups:
|
|
lines.append("@echo ::endgroup::")
|
|
print()
|
|
lines.append("@echo All Pillow dependencies built successfully!")
|
|
write_script("build_dep_all.cmd", lines, prefs, verbose)
|
|
|
|
|
|
def main() -> None:
|
|
winbuild_dir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog="winbuild\\build_prepare.py",
|
|
description="Download and generate build scripts for Pillow dependencies.",
|
|
epilog="""Arguments can also be supplied using the environment variables
|
|
PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\\build.rst
|
|
for more information.""",
|
|
)
|
|
parser.add_argument(
|
|
"-v", "--verbose", action="store_true", help="print generated scripts"
|
|
)
|
|
parser.add_argument(
|
|
"-d",
|
|
"--dir",
|
|
"--build-dir",
|
|
dest="build_dir",
|
|
metavar="PILLOW_BUILD",
|
|
default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")),
|
|
help="build directory (default: 'winbuild\\build')",
|
|
)
|
|
parser.add_argument(
|
|
"--depends",
|
|
dest="depends_dir",
|
|
metavar="PILLOW_DEPS",
|
|
default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")),
|
|
help="directory used to store cached dependencies "
|
|
"(default: 'winbuild\\depends')",
|
|
)
|
|
parser.add_argument(
|
|
"--architecture",
|
|
choices=ARCHITECTURES,
|
|
default=os.environ.get(
|
|
"ARCHITECTURE",
|
|
(
|
|
"ARM64"
|
|
if platform.machine() == "ARM64"
|
|
else ("x86" if struct.calcsize("P") == 4 else "AMD64")
|
|
),
|
|
),
|
|
help="build architecture (default: same as host Python)",
|
|
)
|
|
parser.add_argument(
|
|
"--nmake",
|
|
dest="cmake_generator",
|
|
action="store_const",
|
|
const="NMake Makefiles",
|
|
default="Ninja",
|
|
help="build dependencies using NMake instead of Ninja",
|
|
)
|
|
parser.add_argument(
|
|
"--no-imagequant",
|
|
action="store_true",
|
|
help="skip GPL-licensed optional dependency libimagequant",
|
|
)
|
|
parser.add_argument(
|
|
"--no-fribidi",
|
|
"--no-raqm",
|
|
action="store_true",
|
|
help="skip LGPL-licensed optional dependency FriBiDi",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
arch_prefs = ARCHITECTURES[args.architecture]
|
|
print("Target architecture:", args.architecture)
|
|
|
|
msvs = find_msvs(args.architecture)
|
|
if msvs is None:
|
|
msg = "Visual Studio not found. Please install Visual Studio 2017 or newer."
|
|
raise RuntimeError(msg)
|
|
print("Found Visual Studio at:", msvs["vs_dir"])
|
|
|
|
# dependency cache directory
|
|
args.depends_dir = os.path.abspath(args.depends_dir)
|
|
os.makedirs(args.depends_dir, exist_ok=True)
|
|
print("Caching dependencies in:", args.depends_dir)
|
|
|
|
args.build_dir = os.path.abspath(args.build_dir)
|
|
print("Using output directory:", args.build_dir)
|
|
|
|
# build directory for *.h files
|
|
inc_dir = os.path.join(args.build_dir, "inc")
|
|
# build directory for *.lib files
|
|
lib_dir = os.path.join(args.build_dir, "lib")
|
|
# build directory for *.bin files
|
|
bin_dir = os.path.join(args.build_dir, "bin")
|
|
# directory for storing project files
|
|
sources_dir = os.path.join(args.build_dir, "src")
|
|
# copy dependency licenses to this directory
|
|
license_dir = os.path.join(args.build_dir, "license")
|
|
|
|
shutil.rmtree(args.build_dir, ignore_errors=True)
|
|
os.makedirs(args.build_dir, exist_ok=False)
|
|
for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]:
|
|
os.makedirs(path, exist_ok=True)
|
|
|
|
disabled = []
|
|
if args.no_imagequant:
|
|
disabled += ["libimagequant"]
|
|
if args.no_fribidi:
|
|
disabled += ["fribidi"]
|
|
|
|
prefs = {
|
|
"architecture": args.architecture,
|
|
**arch_prefs,
|
|
# Pillow paths
|
|
"winbuild_dir": winbuild_dir,
|
|
# Build paths
|
|
"bin_dir": bin_dir,
|
|
"build_dir": args.build_dir,
|
|
"depends_dir": args.depends_dir,
|
|
"inc_dir": inc_dir,
|
|
"lib_dir": lib_dir,
|
|
"license_dir": license_dir,
|
|
"src_dir": sources_dir,
|
|
# Compilers / Tools
|
|
**msvs,
|
|
"cmake": "cmake.exe", # TODO find CMAKE automatically
|
|
"cmake_generator": args.cmake_generator,
|
|
# TODO find NASM automatically
|
|
}
|
|
|
|
for k, v in DEPS.items():
|
|
prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"])
|
|
|
|
print()
|
|
|
|
write_script(".gitignore", ["*"], prefs, args.verbose)
|
|
build_env(prefs, args.verbose)
|
|
build_dep_all(disabled, prefs, args.verbose)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|