121 lines
6.6 KiB
Python
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)
|