makepad/tools/makepad_builder/cross-windows.py

121 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Build the portable Windows runner on Linux using Rust's bundled LLD.
SDK setup is an explicit separate step:
cargo build --release -p makepad-loader --no-default-features --bin makepad-builder-cli
target/release/makepad-builder-cli windows-sdk --root ~/loader-cross/windows --accept-ms-license
rustup target add x86_64-pc-windows-msvc
python3 tools/makepad_builder/cross-windows.py --sdk-root ~/loader-cross/windows
No Windows machine, Wine, MSBuild or system-wide SDK install is needed.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import subprocess
import sys
import zipfile
TARGET = "x86_64-pc-windows-msvc"
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--sdk-root", required=True, type=Path)
parser.add_argument("--target-dir", type=Path)
parser.add_argument("--out", type=Path)
args = parser.parse_args()
if platform.system() != "Linux":
parser.error("This build path is for Linux hosts.")
source = Path(__file__).resolve().parents[2]
sdk = args.sdk_root.expanduser().resolve() / "sdk"
target_dir = (args.target_dir or source / "target").resolve()
output = (args.out or target_dir / "loader-runners" / "windows-x86_64.zip").resolve()
sysroot = Path(subprocess.check_output(["rustc", "--print", "sysroot"], text=True).strip())
host = next(line.removeprefix("host: ") for line in subprocess.check_output(["rustc", "-vV"], text=True).splitlines() if line.startswith("host: "))
lld = sysroot / "lib/rustlib" / host / "bin/rust-lld"
if not lld.is_file() or not (sysroot / "lib/rustlib" / TARGET / "lib").is_dir():
parser.error("Rust's LLD and the Windows standard library are required. Run: rustup target add " + TARGET)
# The Windows loader has already verified package hashes and unpacked these.
roots = []
for parent, sentinel in ((sdk / "VC/Tools/MSVC", "lib/x64/libcmt.lib"),
(sdk / "Windows Kits/10/Lib", "um/x64/kernel32.lib"),
(sdk / "Windows Kits/10/Lib", "ucrt/x64/libucrt.lib")):
found = []
if parent.is_dir():
for version in parent.iterdir():
folder = version / Path(sentinel).parent
if folder.is_dir() and any(p.name.lower() == Path(sentinel).name for p in folder.iterdir()):
found.append(folder)
if len(found) != 1:
parser.error("Expected one extracted SDK version containing " + sentinel + ". Run windows-sdk first, or use a version-specific SDK root.")
roots.append(found[0])
# DEFAULTLIB directives use Windows' case-insensitive names. Provide a
# lowercase index on Linux without modifying the downloaded SDK payloads.
libraries = args.sdk_root.expanduser().resolve() / "link-libs"
libraries.mkdir(parents=True, exist_ok=True)
for folder in roots:
for path in folder.iterdir():
if path.is_file() and path.suffix.lower() == ".lib":
name = libraries / path.name.lower()
if name.is_symlink():
if name.resolve() == path.resolve():
continue
parser.error("Conflicting SDK library: " + name.name)
if name.exists():
parser.error("SDK link index contains a non-link: " + str(name))
name.symlink_to(path)
env = os.environ.copy()
env["CARGO_TARGET_DIR"] = str(target_dir)
env["MAKEPAD_PACKAGE_DIR"] = "."
env["CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER"] = str(lld)
env["CARGO_ENCODED_RUSTFLAGS"] = "\x1f".join(["-C", "linker-flavor=lld-link", "-C", "target-feature=+crt-static", "-L", "native=" + str(libraries)])
# The Builder is downloaded by everyone and built here once, so build for
# size: whole-program LTO in one codegen unit at opt-level z (measured
# 29.8 -> 21.8 MB before the widget trim; build time does not matter here).
env["CARGO_PROFILE_RELEASE_LTO"] = "fat"
env["CARGO_PROFILE_RELEASE_CODEGEN_UNITS"] = "1"
env["CARGO_PROFILE_RELEASE_OPT_LEVEL"] = "z"
common = ["-p", "makepad-loader", "--target", TARGET]
subprocess.run(["cargo", "check"] + common, cwd=source, env=env, check=True)
# Distribution executables do not carry PDBs. Strip at the final link so
# LLD does not look for Microsoft's private build-machine debug databases.
for binary in ("makepad-builder",):
subprocess.run(["cargo", "rustc", "--release"] + common + ["--bin", binary, "--", "-C", "strip=symbols", "-C", "link-arg=/DEBUG:NONE"], cwd=source, env=env, check=True)
output.parent.mkdir(parents=True, exist_ok=True)
temporary = output.with_suffix(".zip.part")
binaries = {}
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as archive:
for name in ("makepad-builder.exe",):
executable = target_dir / TARGET / "release" / name
if executable.read_bytes()[:2] != b"MZ":
raise RuntimeError("Build did not produce a Windows executable: " + name)
archive.write(executable, name)
binaries[name] = hashlib.sha256(executable.read_bytes()).hexdigest()
# Fonts are runtime assets, not embedded by app_main's manifest. Ship
# the widget resource set except the large CJK and colour-emoji
# fallbacks (about 48 MB): the Builder window (src/app.rs) leaves the
# emoji member out of its terminal fonts and never draws CJK, and the
# widgets load those two families only when such text appears.
excluded = {"LXGWWenKaiRegular.ttf", "LXGWWenKaiBold.ttf", "NotoColorEmoji.ttf"}
resources = source / "widgets/resources"
for asset in sorted(resources.rglob("*")):
if asset.is_file() and asset.name not in excluded:
archive.write(asset, "makepad_widgets/resources/" + asset.relative_to(resources).as_posix())
temporary.replace(output)
evidence = {"target": TARGET, "rust": subprocess.check_output(["rustc", "--version"], text=True).strip(),
"sdk_libraries": [str(p) for p in roots], "binaries": binaries,
"zip_sha256": hashlib.sha256(output.read_bytes()).hexdigest()}
output.with_suffix(".build.json").write_text(json.dumps(evidence, indent=2) + "\n")
print("\nBuilt " + str(output))
print("Validate this ZIP on Windows before publishing it.")
if __name__ == "__main__":
try:
main()
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
print("Windows cross-build failed: " + str(error), file=sys.stderr)
sys.exit(1)