95dbf30fb9
All platforms: - rename scripts/ to tools/: Bazelisk expects to find its wrapper script (used by the Mac changes below) in tools/. Rather than have a separate scripts/ and tools/, it's simpler to just move everything into tools/. - wheel outputs and binary bundles now go into .bazel/out/dist. While not technically Bazel build products, doing it this way ensures they get cleaned up when 'bazel clean' is run, and it keeps them out of the source folder. - update to the latest Bazel Windows changes: - bazel.bat has been removed, and tools\setup-env.bat has been added. Other scripts like .\run.bat will automatically call it to set up the environment. - because Bazel is now on the path, you can 'bazel test ...' from any folder, instead of having to do \anki\bazel. - the bat files can handle being called from any working directory, so things like running "\anki\tools\python" from c:\ will work. - build installer as part of bundling process Mac changes: - `arch -arch x86_64 bazel ...` will now automatically use a different build root, so that it is cheap to switch back and forth between archs on a new Mac. - tools/run-qt* will now automatically use Rosetta - disable jemalloc in Mac x86 build for now, as it won't build under Rosetta (perhaps due to its build scripts using $host_cpu instead of $target_cpu) - create app bundle as part of bundling process Linux changes: - remove arm64 orjson workaround in Linux bundle, as without a readily-available, relatively distro-agonstic PyQt/Qt build we can use, the arm64 Linux bundle is of very limited usefulness. - update Docker files for release build - include fcitx5 in both the qt5 and qt6 bundles - create tarballs as part of the bundling process
396 lines
12 KiB
Python
396 lines
12 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
import glob
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
is_win = sys.platform == "win32"
|
|
is_mac = sys.platform == "darwin"
|
|
|
|
workspace = Path(sys.argv[1])
|
|
bazel_external = Path(sys.argv[2])
|
|
|
|
|
|
def with_exe_extension(program: str) -> str:
|
|
if is_win:
|
|
return program + ".exe"
|
|
else:
|
|
return program
|
|
|
|
|
|
output_root = workspace / ".bazel" / "out" / "build"
|
|
dist_folder = output_root / ".." / "dist"
|
|
venv = output_root / f"venv-{platform.machine()}"
|
|
build_folder = output_root / f"build-{platform.machine()}"
|
|
cargo_target = output_root / f"target-{platform.machine()}"
|
|
artifacts = output_root / "artifacts"
|
|
pyo3_config = output_root / "pyo3-build-config-file.txt"
|
|
pyoxidizer_folder = bazel_external / "pyoxidizer"
|
|
arm64_protobuf_wheel = bazel_external / "protobuf_wheel_mac_arm64"
|
|
pyoxidizer_binary = cargo_target / "release" / with_exe_extension("pyoxidizer")
|
|
|
|
for path in dist_folder.glob("*.zst"):
|
|
path.unlink()
|
|
|
|
os.environ["PYOXIDIZER_ARTIFACT_DIR"] = str(artifacts)
|
|
os.environ["PYOXIDIZER_CONFIG"] = str(Path(os.getcwd()) / "pyoxidizer.bzl")
|
|
os.environ["CARGO_TARGET_DIR"] = str(cargo_target)
|
|
|
|
# OS-specific things
|
|
pyqt5_folder_name = "pyqt515"
|
|
pyqt6_folder_path = bazel_external / "pyqt6" / "PyQt6"
|
|
extra_linux_deps = bazel_external / "bundle_extras_linux_amd64"
|
|
extra_qt5_linux_plugins = extra_linux_deps / "qt5"
|
|
extra_qt6_linux_plugins = extra_linux_deps / "qt6"
|
|
is_lin = False
|
|
arm64_linux = arm64_mac = False
|
|
if is_win:
|
|
os.environ["TARGET"] = "x86_64-pc-windows-msvc"
|
|
elif sys.platform.startswith("darwin"):
|
|
if platform.machine() == "arm64":
|
|
arm64_mac = True
|
|
pyqt5_folder_name = None
|
|
os.environ["TARGET"] = "aarch64-apple-darwin"
|
|
os.environ["MACOSX_DEPLOYMENT_TARGET"] = "11.0"
|
|
else:
|
|
pyqt5_folder_name = "pyqt514"
|
|
os.environ["TARGET"] = "x86_64-apple-darwin"
|
|
os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.13"
|
|
else:
|
|
is_lin = True
|
|
if platform.machine() == "x86_64":
|
|
os.environ["TARGET"] = "x86_64-unknown-linux-gnu"
|
|
else:
|
|
os.environ["TARGET"] = "aarch64-unknown-linux-gnu"
|
|
pyqt5_folder_name = None
|
|
pyqt6_folder_path = None
|
|
arm64_linux = True
|
|
|
|
if is_win:
|
|
python_bin_folder = venv / "scripts"
|
|
os.environ["PATH"] += rf";{os.getenv('USERPROFILE')}\.cargo\bin"
|
|
cargo_features = "build-mode-prebuilt-artifacts"
|
|
else:
|
|
python_bin_folder = venv / "bin"
|
|
# PyOxidizer build depends on a system-installed version of Python,
|
|
# as the standalone build does not have its config set up properly,
|
|
# leading to "directory not found for option '-L/install/lib'".
|
|
# On macOS, after installing a system Python in /usr/local/bin,
|
|
# make sure /usr/local/bin/python3 is symlinked to /usr/local/bin/python.
|
|
os.environ["PATH"] = ":".join(
|
|
["/usr/local/bin", f"{os.getenv('HOME')}/.cargo/bin", os.getenv("PATH")]
|
|
)
|
|
cargo_features = "build-mode-prebuilt-artifacts"
|
|
if not is_mac or arm64_mac:
|
|
cargo_features += " global-allocator-jemalloc allocator-jemalloc"
|
|
|
|
python = python_bin_folder / with_exe_extension("python")
|
|
pip = python_bin_folder / with_exe_extension("pip")
|
|
artifacts_in_build = (
|
|
build_folder / os.getenv("TARGET") / "release" / "resources" / "extra_files"
|
|
)
|
|
|
|
|
|
def build_pyoxidizer():
|
|
pyoxidizer_folder_mtime = pyoxidizer_folder.stat().st_mtime
|
|
if (
|
|
pyoxidizer_binary.exists()
|
|
and pyoxidizer_binary.stat().st_mtime == pyoxidizer_folder_mtime
|
|
):
|
|
# avoid recompiling if pyoxidizer folder has not changed
|
|
return
|
|
subprocess.run(
|
|
[
|
|
"cargo",
|
|
"build",
|
|
"--release",
|
|
],
|
|
cwd=pyoxidizer_folder,
|
|
check=True,
|
|
)
|
|
os.utime(pyoxidizer_binary, (pyoxidizer_folder_mtime, pyoxidizer_folder_mtime))
|
|
|
|
|
|
def install_wheels_into_venv():
|
|
# Pip's handling of hashes is somewhat broken. It spots the hashes in the constraints
|
|
# file and forces all files to have a hash. We can manually hash our generated wheels
|
|
# and pass them in with hashes, but it still breaks, because the 'protobuf>=3.17'
|
|
# specifier in the pylib wheel is not allowed. Nevermind that a specific version is
|
|
# included in the constraints file we pass along! To get things working, we're
|
|
# forced to strip the hashes out before installing. This should be safe, as the files
|
|
# have already been validated as part of the build process.
|
|
constraints = output_root / "deps_without_hashes.txt"
|
|
with open(workspace / "python" / "requirements.txt") as f:
|
|
buf = f.read()
|
|
with open(constraints, "w") as f:
|
|
extracted = re.findall("^(\S+==\S+) ", buf, flags=re.M)
|
|
extracted = [
|
|
line for line in extracted if not arm64_mac or "protobuf" not in line
|
|
]
|
|
f.write("\n".join(extracted))
|
|
# pypi protobuf lacks C extension on darwin-arm64, so we have to use a version
|
|
# we built ourselves
|
|
if arm64_mac:
|
|
wheels = glob.glob(str(arm64_protobuf_wheel / "*.whl"))
|
|
subprocess.run(
|
|
[pip, "install", "--upgrade", "-c", constraints, *wheels], check=True
|
|
)
|
|
# install wheels and upgrade any deps
|
|
wheels = glob.glob(str(workspace / ".bazel" / "out" / "dist" / "*.whl"))
|
|
subprocess.run(
|
|
[pip, "install", "--upgrade", "-c", constraints, *wheels], check=True
|
|
)
|
|
# always reinstall our wheels
|
|
subprocess.run(
|
|
[pip, "install", "--force-reinstall", "--no-deps", *wheels], check=True
|
|
)
|
|
|
|
|
|
def build_artifacts():
|
|
if os.path.exists(artifacts):
|
|
shutil.rmtree(artifacts)
|
|
if os.path.exists(artifacts_in_build):
|
|
shutil.rmtree(artifacts_in_build)
|
|
|
|
subprocess.run(
|
|
[
|
|
pyoxidizer_binary,
|
|
"--system-rust",
|
|
"run-build-script",
|
|
"build.rs",
|
|
"--var",
|
|
"venv",
|
|
venv,
|
|
"--var",
|
|
"build",
|
|
build_folder,
|
|
],
|
|
check=True,
|
|
env=os.environ
|
|
| dict(
|
|
CARGO_MANIFEST_DIR=".",
|
|
OUT_DIR=str(artifacts),
|
|
PROFILE="release",
|
|
PYO3_PYTHON=str(python),
|
|
),
|
|
)
|
|
|
|
existing_config = None
|
|
if os.path.exists(pyo3_config):
|
|
with open(pyo3_config) as f:
|
|
existing_config = f.read()
|
|
|
|
with open(artifacts / "pyo3-build-config-file.txt") as f:
|
|
new_config = f.read()
|
|
|
|
# avoid bumping mtime, which triggers crate recompile
|
|
if new_config != existing_config:
|
|
with open(pyo3_config, "w") as f:
|
|
f.write(new_config)
|
|
|
|
|
|
def build_pkg():
|
|
subprocess.run(
|
|
[
|
|
"cargo",
|
|
"build",
|
|
"--release",
|
|
"--no-default-features",
|
|
"--features",
|
|
cargo_features,
|
|
],
|
|
check=True,
|
|
env=os.environ | dict(PYO3_CONFIG_FILE=str(pyo3_config)),
|
|
)
|
|
|
|
|
|
def adj_path_for_windows_rsync(path: Path) -> str:
|
|
if not is_win:
|
|
return str(path)
|
|
|
|
path = path.absolute()
|
|
rest = str(path)[2:].replace("\\", "/")
|
|
return f"/{path.drive[0]}{rest}"
|
|
|
|
|
|
def merge_into_dist(output_folder: Path, pyqt_src_path: Path | None):
|
|
if output_folder.exists():
|
|
shutil.rmtree(output_folder)
|
|
output_folder.mkdir(parents=True)
|
|
# PyQt
|
|
if pyqt_src_path and not is_mac:
|
|
subprocess.run(
|
|
[
|
|
"rsync",
|
|
"-a",
|
|
"--delete",
|
|
"--exclude-from",
|
|
"qt.exclude",
|
|
adj_path_for_windows_rsync(pyqt_src_path),
|
|
adj_path_for_windows_rsync(output_folder / "lib") + "/",
|
|
],
|
|
check=True,
|
|
)
|
|
if is_lin:
|
|
if "PyQt5" in str(pyqt_src_path):
|
|
src = extra_qt5_linux_plugins
|
|
dest = output_folder / "lib" / "PyQt5" / "Qt5" / "plugins"
|
|
else:
|
|
src = extra_qt6_linux_plugins
|
|
dest = output_folder / "lib" / "PyQt6" / "Qt6" / "plugins"
|
|
subprocess.run(
|
|
["rsync", "-a", str(src) + "/", str(dest) + "/"],
|
|
check=True,
|
|
)
|
|
|
|
# Executable and other resources
|
|
resources = [
|
|
adj_path_for_windows_rsync(
|
|
cargo_target / "release" / ("anki.exe" if is_win else "anki")
|
|
),
|
|
adj_path_for_windows_rsync(artifacts_in_build) + "/",
|
|
]
|
|
if is_lin:
|
|
resources.append("lin/")
|
|
|
|
subprocess.run(
|
|
[
|
|
"rsync",
|
|
"-a",
|
|
"--delete",
|
|
"--exclude",
|
|
"PyQt6",
|
|
"--exclude",
|
|
"PyQt5",
|
|
*resources,
|
|
adj_path_for_windows_rsync(output_folder) + "/",
|
|
],
|
|
check=True,
|
|
)
|
|
# Ensure all files are world-readable
|
|
if not is_win:
|
|
subprocess.run(["chmod", "-R", "a+r", output_folder])
|
|
|
|
|
|
def anki_version() -> str:
|
|
with open(workspace / "defs.bzl") as fobj:
|
|
data = fobj.read()
|
|
return re.search('^anki_version = "(.*)"$', data, re.MULTILINE).group(1)
|
|
|
|
|
|
def annotated_linux_folder_name(variant: str) -> str:
|
|
components = ["anki", anki_version(), "linux", variant]
|
|
return "-".join(components)
|
|
|
|
|
|
def annotated_mac_dmg_name(variant: str) -> str:
|
|
if platform.machine() == "arm64":
|
|
arch = "apple"
|
|
else:
|
|
arch = "intel"
|
|
components = ["anki", anki_version(), "mac", arch, variant]
|
|
return "-".join(components)
|
|
|
|
|
|
def build_bundle(src_path: Path, variant: str) -> None:
|
|
if is_lin:
|
|
print("--- Build tarball")
|
|
build_tarball(src_path, variant)
|
|
elif is_mac:
|
|
print("--- Build app bundle")
|
|
build_app_bundle(src_path, variant)
|
|
|
|
|
|
def build_app_bundle(src_path: Path, variant: str) -> None:
|
|
if arm64_mac:
|
|
variant = "qt6_arm64"
|
|
else:
|
|
variant = f"{variant}_amd64"
|
|
subprocess.run(
|
|
["cargo", "run", variant, src_path, anki_version(), bazel_external],
|
|
check=True,
|
|
cwd=workspace / "qt" / "bundle" / "mac",
|
|
)
|
|
variant_path = src_path.parent / "app" / variant
|
|
if os.getenv("NOTARIZE_USER"):
|
|
subprocess.run(
|
|
["python", "mac/notarize.py", "upload", variant_path],
|
|
check=True,
|
|
)
|
|
# note down the dmg name for later
|
|
open(variant_path / "dmg_name", "w").write(
|
|
annotated_mac_dmg_name(variant[0:3]) + ".dmg"
|
|
)
|
|
|
|
|
|
def build_tarball(src_path: Path, variant: str) -> None:
|
|
if not is_lin:
|
|
return
|
|
dest_path = src_path.with_name(annotated_linux_folder_name(variant))
|
|
if dest_path.exists():
|
|
shutil.rmtree(dest_path)
|
|
os.rename(src_path, dest_path)
|
|
print("compress", dest_path.name, "...")
|
|
subprocess.run(
|
|
[
|
|
"tar",
|
|
"--zstd",
|
|
"-cf",
|
|
dist_folder / (dest_path.name + ".tar.zst"),
|
|
dest_path.name,
|
|
],
|
|
check=True,
|
|
env=dict(ZSTD_CLEVEL="9"),
|
|
cwd=dest_path.parent,
|
|
)
|
|
|
|
|
|
def build_windows_installers() -> None:
|
|
subprocess.run(
|
|
[
|
|
"cargo",
|
|
"run",
|
|
output_root,
|
|
bazel_external,
|
|
Path(__file__).parent,
|
|
anki_version(),
|
|
],
|
|
check=True,
|
|
cwd=workspace / "qt" / "bundle" / "win",
|
|
)
|
|
|
|
|
|
print("--- Build PyOxidizer")
|
|
build_pyoxidizer()
|
|
print("--- Install wheels into venv")
|
|
install_wheels_into_venv()
|
|
print("--- Build PyOxidizer artifacts")
|
|
build_artifacts()
|
|
print("--- Build Anki binary")
|
|
build_pkg()
|
|
print("--- Copy binary+resources into folder (Qt6)")
|
|
merge_into_dist(output_root / "std", pyqt6_folder_path)
|
|
build_bundle(output_root / "std", "qt6")
|
|
if pyqt5_folder_name:
|
|
print("--- Copy binary+resources into folder (Qt5)")
|
|
merge_into_dist(output_root / "alt", bazel_external / pyqt5_folder_name / "PyQt5")
|
|
build_bundle(output_root / "alt", "qt5")
|
|
|
|
if is_win:
|
|
build_windows_installers()
|
|
|
|
if is_mac:
|
|
print("outputs are in .bazel/out/build/{std,alt}")
|
|
print("dmg can be created with mac/finalize.py dmg")
|
|
else:
|
|
print("outputs are in .bazel/out/dist/")
|