249 lines
9.0 KiB
Python
Executable File
249 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Verify and package the Windows x64 Godot export without executing it."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import verify_windows_native
|
|
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
REPO = HERE.parent
|
|
MANAGED_DIRECTORY = "data_Himegari_windows_x86_64"
|
|
PACKAGE_NAME = "OpenMaidEngine-Himegari-windows-x64"
|
|
REQUIRED_FILES = (
|
|
"Himegari.exe",
|
|
"Himegari.pck",
|
|
f"{MANAGED_DIRECTORY}/Himegari.dll",
|
|
f"{MANAGED_DIRECTORY}/Age.Engine.dll",
|
|
f"{MANAGED_DIRECTORY}/Age.Engine.Frontend.dll",
|
|
f"{MANAGED_DIRECTORY}/Age.Engine.Text.Windows.dll",
|
|
f"{MANAGED_DIRECTORY}/coreclr.dll",
|
|
f"{MANAGED_DIRECTORY}/hostfxr.dll",
|
|
f"{MANAGED_DIRECTORY}/hostpolicy.dll",
|
|
f"{MANAGED_DIRECTORY}/System.Private.CoreLib.dll",
|
|
*(f"{MANAGED_DIRECTORY}/{name}" for name in verify_windows_native.REQUIRED_FILES),
|
|
)
|
|
FORBIDDEN_FILES = (
|
|
"Himegari.x86_64",
|
|
f"{MANAGED_DIRECTORY}/libage_movie_ffmpeg.so",
|
|
f"{MANAGED_DIRECTORY}/libavformat.so.62",
|
|
)
|
|
PROJECT_FILES = ("LICENSE", "README.md", "THIRD_PARTY_NOTICES.md")
|
|
|
|
|
|
def _resolved_directory(path: Path, label: str) -> Path:
|
|
resolved = path.resolve()
|
|
if not resolved.is_dir():
|
|
raise ValueError(f"{label} directory was not found: {resolved}")
|
|
return resolved
|
|
|
|
|
|
def verify_export(export_directory: Path, objdump: str) -> tuple[Path, dict[str, Any]]:
|
|
export_directory = _resolved_directory(export_directory, "Windows export")
|
|
missing = [name for name in REQUIRED_FILES if not (export_directory / name).is_file()]
|
|
if missing:
|
|
raise ValueError("Windows export is incomplete; missing: " + ", ".join(missing))
|
|
forbidden = [name for name in FORBIDDEN_FILES if (export_directory / name).exists()]
|
|
forbidden.extend(
|
|
path.relative_to(export_directory).as_posix()
|
|
for path in export_directory.rglob("*")
|
|
if path.is_file() and (path.name.endswith(".so") or ".so." in path.name)
|
|
)
|
|
if forbidden:
|
|
raise ValueError("Windows export contains Linux-only files: " + ", ".join(sorted(set(forbidden))))
|
|
executable_machine = verify_windows_native.pe_machine(export_directory / "Himegari.exe")
|
|
if executable_machine != verify_windows_native.PE_MACHINE_AMD64:
|
|
raise ValueError(
|
|
f"Windows executable is not AMD64 PE (0x{executable_machine:04x}): Himegari.exe"
|
|
)
|
|
native_report = verify_windows_native.verify_bundle(
|
|
export_directory / MANAGED_DIRECTORY,
|
|
objdump,
|
|
exact_dlls=False,
|
|
)
|
|
native_report["bundle"] = MANAGED_DIRECTORY
|
|
return export_directory, {
|
|
"schema_version": 1,
|
|
"target": "win-x64",
|
|
"executable_machine": "AMD64",
|
|
"native": native_report,
|
|
}
|
|
|
|
|
|
def _git(*arguments: str) -> str:
|
|
result = subprocess.run(
|
|
("git", *arguments), cwd=REPO, check=True, capture_output=True, text=True
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def source_date_epoch() -> int:
|
|
configured = os.environ.get("SOURCE_DATE_EPOCH")
|
|
value = configured or _git("show", "-s", "--format=%ct", "HEAD")
|
|
try:
|
|
epoch = int(value)
|
|
except ValueError as error:
|
|
raise ValueError(f"invalid SOURCE_DATE_EPOCH: {value!r}") from error
|
|
if epoch < 0:
|
|
raise ValueError("SOURCE_DATE_EPOCH must not be negative")
|
|
return epoch
|
|
|
|
|
|
def build_metadata(epoch: int) -> dict[str, object]:
|
|
with (HERE / "godot-linux-x64.json").open(encoding="utf-8") as stream:
|
|
godot = json.load(stream)
|
|
windows_template = next(
|
|
member
|
|
for member in godot["templates"]["members"]
|
|
if member["install_name"] == "windows_release_x86_64.exe"
|
|
)
|
|
with (REPO / "native/age_movie_ffmpeg/dependency-win64.json").open(
|
|
encoding="utf-8"
|
|
) as stream:
|
|
ffmpeg = json.load(stream)
|
|
return {
|
|
"schema_version": 1,
|
|
"project": "OpenMaidEngine Himegari profile",
|
|
"target": "win-x64",
|
|
"source_commit": _git("rev-parse", "HEAD"),
|
|
"source_dirty": bool(_git("status", "--porcelain", "--untracked-files=normal")),
|
|
"source_date_epoch": epoch,
|
|
"godot": {
|
|
"version": godot["godot_version"],
|
|
"editor_archive_sha256": godot["editor"]["sha256"],
|
|
"release_template_sha256": windows_template["sha256"],
|
|
},
|
|
"ffmpeg": {
|
|
"provider": ffmpeg["provider"],
|
|
"version": ffmpeg["ffmpeg_version"],
|
|
"archive_sha256": ffmpeg["sha256"],
|
|
},
|
|
}
|
|
|
|
|
|
def _hash(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _write_text(path: Path, content: str) -> None:
|
|
with path.open("w", encoding="utf-8", newline="\n") as stream:
|
|
stream.write(content)
|
|
|
|
|
|
def _write_checksums(package_root: Path) -> None:
|
|
checksum_path = package_root / "SHA256SUMS"
|
|
files = sorted(
|
|
path for path in package_root.rglob("*")
|
|
if path.is_file() and path != checksum_path
|
|
)
|
|
lines = [f"{_hash(path)} {path.relative_to(package_root).as_posix()}" for path in files]
|
|
_write_text(checksum_path, "\n".join(lines) + "\n")
|
|
|
|
|
|
def _zip_timestamp(epoch: int) -> tuple[int, int, int, int, int, int]:
|
|
minimum = 315532800 # 1980-01-01, the earliest ZIP timestamp.
|
|
maximum = 4354819198 # 2107-12-31 23:59:58, the latest portable ZIP timestamp.
|
|
stamp = time.gmtime(max(minimum, min(epoch, maximum)))[:6]
|
|
return (*stamp[:5], stamp[5] - stamp[5] % 2)
|
|
|
|
|
|
def create_package(
|
|
export_directory: Path,
|
|
output_directory: Path,
|
|
metadata: dict[str, object],
|
|
verification: dict[str, Any],
|
|
epoch: int,
|
|
) -> tuple[Path, Path]:
|
|
export_directory = _resolved_directory(export_directory, "Windows export")
|
|
output_directory = output_directory.resolve()
|
|
expected_parent = (REPO / "build/package/windows-x64").resolve()
|
|
if output_directory != expected_parent:
|
|
raise ValueError(f"refusing to replace unexpected package directory: {output_directory}")
|
|
staging_parent = output_directory / "staging"
|
|
package_root = staging_parent / PACKAGE_NAME
|
|
archive_path = output_directory / f"{PACKAGE_NAME}.zip"
|
|
if staging_parent.exists():
|
|
shutil.rmtree(staging_parent)
|
|
staging_parent.mkdir(parents=True)
|
|
shutil.copytree(export_directory, package_root, copy_function=shutil.copy2)
|
|
for name in PROJECT_FILES:
|
|
source = REPO / name
|
|
if not source.is_file():
|
|
raise ValueError(f"package notice file was not found: {source}")
|
|
shutil.copy2(source, package_root / name)
|
|
_write_text(
|
|
package_root / "BUILD-INFO.json",
|
|
json.dumps(metadata, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
|
)
|
|
_write_text(
|
|
package_root / "WINDOWS-VERIFICATION.json",
|
|
json.dumps(verification, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
|
)
|
|
_write_checksums(package_root)
|
|
|
|
output_directory.mkdir(parents=True, exist_ok=True)
|
|
temporary_archive = archive_path.with_suffix(archive_path.suffix + ".part")
|
|
timestamp = _zip_timestamp(epoch)
|
|
with zipfile.ZipFile(
|
|
temporary_archive, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
|
|
) as archive:
|
|
for path in sorted(item for item in package_root.rglob("*") if item.is_file()):
|
|
relative = path.relative_to(staging_parent).as_posix()
|
|
info = zipfile.ZipInfo(relative, timestamp)
|
|
info.create_system = 3
|
|
info.external_attr = 0o100644 << 16
|
|
info.compress_type = zipfile.ZIP_DEFLATED
|
|
with path.open("rb") as source, archive.open(info, "w", force_zip64=True) as target:
|
|
shutil.copyfileobj(source, target, length=1024 * 1024)
|
|
os.replace(temporary_archive, archive_path)
|
|
return package_root, archive_path
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
verify = subparsers.add_parser("verify")
|
|
verify.add_argument("export_directory", type=Path)
|
|
verify.add_argument("--objdump", default="x86_64-w64-mingw32-objdump")
|
|
package = subparsers.add_parser("package")
|
|
package.add_argument("export_directory", type=Path)
|
|
package.add_argument("--objdump", default="x86_64-w64-mingw32-objdump")
|
|
package.add_argument(
|
|
"--output-directory", type=Path, default=REPO / "build/package/windows-x64"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
export_directory, verification = verify_export(args.export_directory, args.objdump)
|
|
if args.command == "verify":
|
|
print(export_directory)
|
|
return 0
|
|
epoch = source_date_epoch()
|
|
package_root, archive_path = create_package(
|
|
export_directory,
|
|
args.output_directory,
|
|
build_metadata(epoch),
|
|
verification,
|
|
epoch,
|
|
)
|
|
print(package_root)
|
|
print(archive_path)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|