manim/.github/scripts/ci_build_cairo.py
Christian Clauss 0d2533231b
Test on Apple Silicon ARM64 (#4496)
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-01-12 01:22:16 +01:00

237 lines
8.4 KiB
Python

# Logic is as follows:
# 1. Download cairo source code: https://cairographics.org/releases/cairo-<version>.tar.xz
# 2. Verify the downloaded file using the sha256sums file: https://cairographics.org/releases/cairo-<version>.tar.xz.sha256sum
# 3. Extract the downloaded file.
# 4. Create a virtual environment and install meson and ninja.
# 5. Run meson build in the extracted directory. Also, set required prefix.
# 6. Run meson compile -C build.
# 7. Run meson install -C build.
import hashlib
import logging
import os
import subprocess
import sys
import tarfile
import tempfile
import urllib.request
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from sys import stdout
CAIRO_VERSION = "1.18.0"
CAIRO_URL = f"https://cairographics.org/releases/cairo-{CAIRO_VERSION}.tar.xz"
CAIRO_SHA256_URL = f"{CAIRO_URL}.sha256sum"
VENV_NAME = "meson-venv"
BUILD_DIR = "build"
INSTALL_PREFIX = Path(__file__).parent.parent.parent / "third_party" / "cairo"
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
def is_ci():
return os.getenv("CI", None) is not None
def download_file(url, path):
logger.info(f"Downloading {url} to {path}")
block_size = 1024 * 1024
with urllib.request.urlopen(url) as response, open(path, "wb") as file:
while True:
data = response.read(block_size)
if not data:
break
file.write(data)
def verify_sha256sum(path, sha256sum):
with open(path, "rb") as file:
file_hash = hashlib.sha256(file.read()).hexdigest()
if file_hash != sha256sum:
raise Exception("SHA256SUM does not match")
def extract_tar_xz(path, directory):
with tarfile.open(path) as file:
file.extractall(directory)
def run_command(command, cwd=None, env=None):
process = subprocess.Popen(command, cwd=cwd, env=env)
process.communicate()
if process.returncode != 0:
raise Exception("Command failed")
@contextmanager
def gha_group(title: str) -> Generator:
if not is_ci():
yield
return
print(f"\n::group::{title}")
stdout.flush()
try:
yield
finally:
print("::endgroup::")
stdout.flush()
def set_env_var_gha(name: str, value: str) -> None:
if not is_ci():
return
env_file = os.getenv("GITHUB_ENV", None)
if env_file is None:
return
with open(env_file, "a") as file:
file.write(f"{name}={value}\n")
stdout.flush()
def get_ld_library_path(prefix: Path) -> str:
# given a prefix, the ld library path can be found at
# <prefix>/lib/* or sometimes just <prefix>/lib
# this function returns the path to the ld library path
# first, check if the ld library path exists at <prefix>/lib/*
ld_library_paths = list(prefix.glob("lib/*"))
if len(ld_library_paths) == 1:
return ld_library_paths[0].absolute().as_posix()
# if the ld library path does not exist at <prefix>/lib/*,
# return <prefix>/lib
ld_library_path = prefix / "lib"
if ld_library_path.exists():
return ld_library_path.absolute().as_posix()
return ""
def main():
if sys.platform == "win32":
logger.info("Skipping build on windows")
return
with tempfile.TemporaryDirectory() as tmpdir:
with gha_group("Downloading and Extracting Cairo"):
logger.info(f"Downloading cairo version {CAIRO_VERSION}")
download_file(CAIRO_URL, os.path.join(tmpdir, "cairo.tar.xz"))
logger.info("Downloading cairo sha256sum")
download_file(CAIRO_SHA256_URL, os.path.join(tmpdir, "cairo.sha256sum"))
logger.info("Verifying cairo sha256sum")
with open(os.path.join(tmpdir, "cairo.sha256sum")) as file:
sha256sum = file.read().split()[0]
verify_sha256sum(os.path.join(tmpdir, "cairo.tar.xz"), sha256sum)
logger.info("Extracting cairo")
extract_tar_xz(os.path.join(tmpdir, "cairo.tar.xz"), tmpdir)
with gha_group("Installing meson and ninja"):
logger.info("Creating virtual environment")
run_command([sys.executable, "-m", "venv", os.path.join(tmpdir, VENV_NAME)])
logger.info("Installing meson and ninja")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "pip"),
"install",
"meson",
"ninja",
]
)
# Inherit the current environment so PKG_CONFIG_PATH, CFLAGS, LDFLAGS, etc. are preserved.
env_vars = os.environ.copy()
# Prepend the venv bin directory so meson/ninja from the venv are used.
env_vars["PATH"] = f"{os.path.join(tmpdir, VENV_NAME, 'bin')}{os.pathsep}{env_vars.get('PATH','')}"
# Ensure Homebrew-provided pkgconfig and include/lib paths are present on macOS ARM.
if sys.platform == "darwin":
try:
# Try to get specific prefix for lzo (safer for opt path), fall back to generic brew prefix.
brew_prefix = subprocess.check_output(["brew", "--prefix", "lzo"], text=True).strip()
except subprocess.CalledProcessError:
try:
brew_prefix = subprocess.check_output(["brew", "--prefix"], text=True).strip()
except Exception:
brew_prefix = None
if brew_prefix:
# pkg-config files can live in lib/pkgconfig or opt/<pkg>/lib/pkgconfig
pkgconfig_paths = [f"{brew_prefix}/lib/pkgconfig", f"{brew_prefix}/opt/lzo/lib/pkgconfig"]
# merge with any existing PKG_CONFIG_PATH
existing_pc = env_vars.get("PKG_CONFIG_PATH", "")
merged_pc = ":".join([p for p in pkgconfig_paths if p]) + (f":{existing_pc}" if existing_pc else "")
env_vars["PKG_CONFIG_PATH"] = merged_pc
# Ensure compiler & linker flags include brew include/lib
existing_cflags = env_vars.get("CFLAGS", "")
existing_ldflags = env_vars.get("LDFLAGS", "")
env_vars["CFLAGS"] = f"-I{brew_prefix}/include {existing_cflags}".strip()
env_vars["LDFLAGS"] = f"-L{brew_prefix}/lib {existing_ldflags}".strip()
# Debugging: log environment keys relevant to detection
# logger.info(f"env vars for meson: {env_vars}")
with gha_group("Building and Installing Cairo"):
logger.info("Running meson setup")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "meson"),
"setup",
BUILD_DIR,
f"--prefix={INSTALL_PREFIX.absolute().as_posix()}",
"--buildtype=release",
"-Dtests=disabled",
],
cwd=os.path.join(tmpdir, f"cairo-{CAIRO_VERSION}"),
env=env_vars,
)
logger.info("Running meson compile")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "meson"),
"compile",
"-C",
BUILD_DIR,
],
cwd=os.path.join(tmpdir, f"cairo-{CAIRO_VERSION}"),
env=env_vars,
)
logger.info("Running meson install")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "meson"),
"install",
"-C",
BUILD_DIR,
],
cwd=os.path.join(tmpdir, f"cairo-{CAIRO_VERSION}"),
env=env_vars,
)
logger.info(f"Successfully built cairo and installed it to {INSTALL_PREFIX}")
if __name__ == "__main__":
if "--set-env-vars" in sys.argv:
with gha_group("Setting environment variables"):
# append the pkgconfig directory to PKG_CONFIG_PATH
set_env_var_gha(
"PKG_CONFIG_PATH",
f"{Path(get_ld_library_path(INSTALL_PREFIX), 'pkgconfig').as_posix()}{os.pathsep}"
f'{os.getenv("PKG_CONFIG_PATH", "")}',
)
set_env_var_gha(
"LD_LIBRARY_PATH",
f"{get_ld_library_path(INSTALL_PREFIX)}{os.pathsep}"
f'{os.getenv("LD_LIBRARY_PATH", "")}',
)
sys.exit(0)
main()