Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows: compile xz with CMake #6947

Merged
merged 8 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
windows: parse build configuration with argparse
  • Loading branch information
nulano committed Feb 13, 2023
commit eeb7c7c647cacc59189a5ee7891d38f23d19721a
2 changes: 1 addition & 1 deletion .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
- name: Prepare build
if: steps.build-cache.outputs.cache-hit != 'true'
run: |
& python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir
& python.exe winbuild\build_prepare.py -v --python $env:pythonLocation
shell: pwsh

- name: Build dependencies / libjpeg-turbo
Expand Down
66 changes: 37 additions & 29 deletions winbuild/build.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,42 +38,50 @@ Visual Studio is found automatically with ``vswhere.exe``.
Build configuration
-------------------

The following environment variables, if set, will override the default
behaviour of ``build_prepare.py``:

* ``PYTHON`` + ``EXECUTABLE`` point to the target version of Python.
If ``PYTHON`` is unset, the version of Python used to run
``build_prepare.py`` will be used. If only ``PYTHON`` is set,
``EXECUTABLE`` defaults to ``python.exe``.
* ``ARCHITECTURE`` is used to select a ``x86``, ``x64`` or ``ARM64`` build.
By default, uses same architecture as the version of Python used to run ``build_prepare.py``.
* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory
path, used to store generated build scripts and compiled libraries.
**Warning:** This directory is wiped when ``build_prepare.py`` is run.
* ``PILLOW_DEPS`` points to the directory used to store downloaded
dependencies. By default ``winbuild\depends`` is used.

``build_prepare.py`` also supports the following command line parameters:

* ``-v`` will print generated scripts.
* ``--nmake`` will use NMake instead of Ninja for CMake dependencies
* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency
* ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi
(required for Raqm text shaping).
* ``--python=<path>`` and ``--executable=<exe>`` override ``PYTHON`` and ``EXECUTABLE``.
* ``--architecture=<arch>`` overrides ``ARCHITECTURE``.
* ``--dir=<path>`` and ``--depends=<path>`` override ``PILLOW_BUILD``
and ``PILLOW_DEPS``.
Run ``build_prepare.py`` to configure the build::

usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD]
[--depends PILLOW_DEPS]
[--architecture {x86,x64,ARM64}]
[--python PYTHON] [--executable EXECUTABLE]
[--nmake] [--no-imagequant] [--no-fribidi]

Download dependencies and generate build scripts for Pillow.

options:
-h, --help show this help message and exit
-v, --verbose print generated scripts
-d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD
build directory (default: 'winbuild\build')
--depends PILLOW_DEPS
directory used to store cached dependencies (default:
'winbuild\depends')
--architecture {x86,x64,ARM64}
build architecture (default: same as host python)
--python PYTHON Python install directory (default: use host python)
--executable EXECUTABLE
Python executable (default: use host python)
--nmake build dependencies using NMake instead of Ninja
--no-imagequant skip GPL-licensed optional dependency libimagequant
--no-fribidi, --no-raqm
skip LGPL-licensed optional dependency FriBiDi

Arguments can also be supplied using the environment variables PILLOW_BUILD,
PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. See winbuild\build.rst for more
information.

**Warning:** The build directory is wiped when ``build_prepare.py`` is run.

Dependencies
------------

Dependencies will be automatically downloaded by ``build_prepare.py``.
By default, downloaded dependencies are stored in ``winbuild\depends``;
set the ``PILLOW_DEPS`` environment variable to override this location.
use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable
to override this location.

To build all dependencies, run ``winbuild\build\build_dep_all.cmd``,
or run the individual scripts to build each dependency separately.
or run the individual scripts in order to build each dependency separately.

Building Pillow
---------------
Expand Down Expand Up @@ -106,7 +114,7 @@ The following is a simplified version of the script used on AppVeyor:

set PYTHON=C:\Python38\bin
cd /D C:\Pillow\winbuild
C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends
C:\Python37\bin\python.exe build_prepare.py -v --depends C:\pillow-depends
build\build_dep_all.cmd
build\build_pillow.cmd install
cd ..
Expand Down
181 changes: 115 additions & 66 deletions winbuild/build_prepare.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import argparse
import os
import platform
import re
Expand Down Expand Up @@ -434,7 +435,7 @@ def extract_dep(url, filename):
import urllib.request
import zipfile

file = os.path.join(depends_dir, filename)
file = os.path.join(args.depends_dir, filename)
if not os.path.exists(file):
ex = None
for i in range(3):
Expand Down Expand Up @@ -475,12 +476,12 @@ def extract_dep(url, filename):


def write_script(name, lines):
name = os.path.join(build_dir, name)
name = os.path.join(args.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:
if args.verbose:
for line in lines:
print(" " + line)

Expand Down Expand Up @@ -549,11 +550,14 @@ def build_dep(name):
def build_dep_all():
lines = ["@echo on"]
for dep_name in deps:
print()
if dep_name in disabled:
print(f"Skipping disabled dependency {dep_name}")
continue
script = build_dep(dep_name)
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
lines.append("if errorlevel 1 echo Build failed! && exit /B 1")
print()
lines.append("@echo All Pillow dependencies built successfully!")
write_script("build_dep_all.cmd", lines)

Expand All @@ -572,95 +576,138 @@ def build_pillow():


if __name__ == "__main__":
# winbuild directory
winbuild_dir = os.path.dirname(os.path.realpath(__file__))

verbose = False
disabled = []
depends_dir = os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends"))
python_dir = os.environ.get("PYTHON")
python_exe = os.environ.get("EXECUTABLE", "python.exe")
architecture = os.environ.get(
"ARCHITECTURE",
"ARM64"
if platform.machine() == "ARM64"
else ("x86" if struct.calcsize("P") == 4 else "x64"),
pillow_dir = os.path.realpath(os.path.join(winbuild_dir, ".."))

parser = argparse.ArgumentParser(
prog="winbuild\\build_prepare.py",
description="Download dependencies and generate build scripts for Pillow.",
epilog="""Arguments can also be supplied using the environment variables
PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE.
See winbuild\\build.rst for more information.""",
)
build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build"))
cmake_generator = "Ninja"
sources_dir = ""
for arg in sys.argv[1:]:
if arg == "-v":
verbose = True
elif arg == "--nmake":
cmake_generator = "NMake Makefiles"
elif arg == "--no-imagequant":
disabled += ["libimagequant"]
elif arg == "--no-raqm" or arg == "--no-fribidi":
disabled += ["fribidi"]
elif arg.startswith("--depends="):
depends_dir = arg[10:]
elif arg.startswith("--python="):
python_dir = arg[9:]
elif arg.startswith("--executable="):
python_exe = arg[13:]
elif arg.startswith("--architecture="):
architecture = arg[15:]
elif arg.startswith("--dir="):
build_dir = arg[6:]
elif arg == "--srcdir":
sources_dir = os.path.sep + "src"
else:
msg = "Unknown parameter: " + arg
raise ValueError(msg)

# dependency cache directory
os.makedirs(depends_dir, exist_ok=True)
print("Caching dependencies in:", depends_dir)
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')", # noqa: E501
nulano marked this conversation as resolved.
Show resolved Hide resolved
)
parser.add_argument(
"--architecture",
choices=architectures,
default=os.environ.get(
"ARCHITECTURE",
(
"ARM64"
if platform.machine() == "ARM64"
else ("x86" if struct.calcsize("P") == 4 else "x64")
),
),
help="build architecture (default: same as host python)",
)
parser.add_argument(
"--python",
dest="python_dir",
metavar="PYTHON",
default=os.environ.get("PYTHON"),
help="Python install directory (default: use host python)",
nulano marked this conversation as resolved.
Show resolved Hide resolved
)
parser.add_argument(
"--executable",
dest="python_exe",
metavar="EXECUTABLE",
default=os.environ.get("EXECUTABLE", "python.exe"),
help="Python executable (default: use host python)",
nulano marked this conversation as resolved.
Show resolved Hide resolved
)
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()

if python_dir is None:
python_dir = os.path.dirname(os.path.realpath(sys.executable))
python_exe = os.path.basename(sys.executable)
print("Target Python:", os.path.join(python_dir, python_exe))
arch_prefs = architectures[args.architecture]
print("Target Architecture:", args.architecture)
nulano marked this conversation as resolved.
Show resolved Hide resolved

arch_prefs = architectures[architecture]
print("Target Architecture:", architecture)
if args.python_dir is None:
args.python_dir = os.path.dirname(os.path.realpath(sys.executable))
args.python_exe = os.path.basename(sys.executable)
print("Target Python:", os.path.join(args.python_dir, args.python_exe))

msvs = find_msvs()
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"])

print("Using output directory:", build_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(build_dir, "inc")
inc_dir = os.path.join(args.build_dir, "inc")
# build directory for *.lib files
lib_dir = os.path.join(build_dir, "lib")
lib_dir = os.path.join(args.build_dir, "lib")
# build directory for *.bin files
bin_dir = os.path.join(build_dir, "bin")
bin_dir = os.path.join(args.build_dir, "bin")
# directory for storing project files
sources_dir = build_dir + sources_dir
sources_dir = os.path.join(args.build_dir, "src")
# copy dependency licenses to this directory
license_dir = os.path.join(build_dir, "license")
license_dir = os.path.join(args.build_dir, "license")

shutil.rmtree(build_dir, ignore_errors=True)
os.makedirs(build_dir, exist_ok=False)
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 = {
# Python paths / preferences
"python_dir": python_dir,
"python_exe": python_exe,
"architecture": architecture,
"python_dir": args.python_dir,
"python_exe": args.python_exe,
"architecture": args.architecture,
**arch_prefs,
# Pillow paths
"pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")),
"pillow_dir": pillow_dir,
"winbuild_dir": winbuild_dir,
# Build paths
"build_dir": build_dir,
"build_dir": args.build_dir,
"inc_dir": inc_dir,
"lib_dir": lib_dir,
"bin_dir": bin_dir,
Expand All @@ -669,7 +716,7 @@ def build_pillow():
# Compilers / Tools
**msvs,
"cmake": "cmake.exe", # TODO find CMAKE automatically
"cmake_generator": cmake_generator,
"cmake_generator": args.cmake_generator,
# TODO find NASM automatically
# script header
"header": sum([header, msvs["header"], ["@echo on"]], []),
Expand All @@ -682,4 +729,6 @@ def build_pillow():

write_script(".gitignore", ["*"])
build_dep_all()
if args.verbose:
print()
build_pillow()