"""Publishes a WebGL2 WASM project in Release and zips it into an itch.io-uploadable package. Versions the package by the Darwinbots3 SVN revision the build was made from. """ import argparse import os import re import shutil import subprocess import sys import zipfile REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SOURCE_ROOTS = ["Modules", "Games"] REVISION_ROOTS = ["Modules", "3rdParty"] COMPRESSED_SUFFIXES = (".br", ".gz") def find_project_directory(project): """Locates the project's directory.""" csproj_name = project + ".csproj" for source_root in SOURCE_ROOTS: source_path = os.path.join(REPO_ROOT, source_root) if not os.path.isdir(source_path): continue for directory_path, directory_names, file_names in os.walk(source_path): if ".svn" in directory_names: directory_names.remove(".svn") if csproj_name in file_names and "wwwroot" in directory_names: return directory_path raise SystemExit(f"Couldn't find a project directory for '{project}' (looked for a folder " f"containing both {csproj_name} and a wwwroot\\ under " f"{'/'.join(SOURCE_ROOTS)}).") def check_clean(allow_dirty): """Returns whether the source working copies had local modifications.""" dirty_lines = [] for revision_root in REVISION_ROOTS: result = subprocess.run(["svn", "status", os.path.join(REPO_ROOT, revision_root)], capture_output=True, text=True, check=True) dirty_lines += [line for line in result.stdout.splitlines() if line.strip()] if dirty_lines and not allow_dirty: raise SystemExit( "Working copy has local modifications; the packaged revision number wouldn't " "reflect what's actually in the zip:\n" + "\n".join(dirty_lines) + "\n\nPass --allow-dirty to package anyway.") return bool(dirty_lines) def svn_revision(path): result = subprocess.run(["svn", "info", path], capture_output=True, text=True, check=True) for line in result.stdout.splitlines(): match = re.match(r"Revision:\s*(\d+)", line) if match: return int(match.group(1)) raise SystemExit(f"Couldn't parse an SVN revision out of 'svn info {path}'.") def run_build(project, configuration): build_script = os.path.join(REPO_ROOT, "Scripts", "build.py") subprocess.run([sys.executable, "-u", build_script, "--project", project, "--configuration", configuration, "--no-interactive"], check=True) def publish_directory(project, configuration): return os.path.join(REPO_ROOT, "Junk", project, configuration.capitalize(), "publish") def run_publish(project_directory, project, configuration): shutil.rmtree(publish_directory(project, configuration), ignore_errors=True) subprocess.run(["dotnet", "publish", "-c", configuration.capitalize(), "-p:CompressionEnabled=false"], cwd=project_directory, check=True) def create_zip(wwwroot, output_path): with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as archive: for directory_path, _directory_names, file_names in os.walk(wwwroot): for file_name in file_names: if file_name.endswith(COMPRESSED_SUFFIXES): continue file_path = os.path.join(directory_path, file_name) archive_name = os.path.relpath(file_path, wwwroot).replace(os.sep, "/") archive.write(file_path, archive_name) def package(project, configuration, allow_dirty): project_directory = find_project_directory(project) is_dirty = check_clean(allow_dirty) revision = svn_revision(os.path.join(REPO_ROOT, "Modules")) run_build(project, configuration) run_publish(project_directory, project, configuration) wwwroot = os.path.join(publish_directory(project, configuration), "wwwroot") if not os.path.isfile(os.path.join(wwwroot, "index.html")): raise SystemExit(f"Expected a published site with an index.html at {wwwroot}, but didn't " f"find one.") output_directory = os.path.join(REPO_ROOT, "Installs") os.makedirs(output_directory, exist_ok=True) suffix = "-dirty" if is_dirty else "" output_path = os.path.join(output_directory, f"{project}-r{revision}{suffix}.zip") create_zip(wwwroot, output_path) print(f"Wrote {output_path} ({os.path.getsize(output_path):,} bytes)") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--project", default="Blacklight.WebGL2.Testbed", help="The WASM project to package (matches its .csproj name).") parser.add_argument("--configuration", default="release", help="[release|debug]") parser.add_argument("--allow-dirty", dest="allow_dirty", action="store_true", help="Package even if Modules/3rdParty have local modifications.") args = parser.parse_args() package(args.project, args.configuration, args.allow_dirty) if __name__ == "__main__": main()