|
| 1 | +"""Compile C# snippets and templates against Unity reference assemblies. |
| 2 | +
|
| 3 | +Each snippet file and each template folder is compiled on its own (they reuse |
| 4 | +type names such as GameManager), twice: |
| 5 | +
|
| 6 | +- player pass: UnityEngine modules only, no UNITY_EDITOR define. Fails if |
| 7 | + editor-only code leaks into what would be a player build. |
| 8 | +- editor pass: UNITY_EDITOR defined and UnityEditor modules referenced. |
| 9 | +
|
| 10 | +Uses of [Obsolete] APIs (CS0618) are errors so snippets stay on modern APIs. |
| 11 | +
|
| 12 | +Usage: |
| 13 | + python compile_csharp.py --unity <Editor/Data> --inputsystem <package dir> |
| 14 | +
|
| 15 | +The Editor/Data folder only needs Managed/UnityEngine/*.dll and |
| 16 | +NetStandard/ref/2.1.0/netstandard.dll (see fetch_unity_refs.py). |
| 17 | +""" |
| 18 | + |
| 19 | +import argparse |
| 20 | +import os |
| 21 | +import re |
| 22 | +import shutil |
| 23 | +import subprocess |
| 24 | +import sys |
| 25 | +import tempfile |
| 26 | +from pathlib import Path |
| 27 | + |
| 28 | +ROOT = Path(__file__).resolve().parents[2] |
| 29 | +LANG_VERSION = "9" |
| 30 | + |
| 31 | +# Unity version defines for a 6000.0 editor |
| 32 | +UNITY_DEFINES = ( |
| 33 | + [f"UNITY_5_{m}_OR_NEWER" for m in range(3, 7)] |
| 34 | + + [f"UNITY_{y}_{m}_OR_NEWER" for y in range(2017, 2024) for m in range(1, 5)] |
| 35 | + + ["UNITY_6000_0_OR_NEWER", "UNITY_6000_0", "UNITY_6000", "ENABLE_INPUT_SYSTEM"] |
| 36 | +) |
| 37 | + |
| 38 | +# Unity serializes private fields, so "never assigned" and "unused" warnings are noise |
| 39 | +NOWARN = "0169,0414,0649,1701,1702" |
| 40 | + |
| 41 | + |
| 42 | +def find_csc() -> list: |
| 43 | + override = os.environ.get("CSC") |
| 44 | + if override: |
| 45 | + return [override] |
| 46 | + dotnet = shutil.which("dotnet") |
| 47 | + if not dotnet: |
| 48 | + sys.exit("dotnet SDK not found; install it or set CSC to a csc executable") |
| 49 | + sdks = subprocess.run([dotnet, "--list-sdks"], capture_output=True, text=True, check=True).stdout |
| 50 | + candidates = [] |
| 51 | + for line in sdks.splitlines(): |
| 52 | + match = re.match(r"(\S+) \[(.+)\]", line.strip()) |
| 53 | + if match: |
| 54 | + csc = Path(match.group(2)) / match.group(1) / "Roslyn" / "bincore" / "csc.dll" |
| 55 | + if csc.exists(): |
| 56 | + candidates.append(csc) |
| 57 | + if not candidates: |
| 58 | + sys.exit("csc.dll not found in any installed .NET SDK") |
| 59 | + return [dotnet, str(candidates[-1])] |
| 60 | + |
| 61 | + |
| 62 | +def compile_unit(csc, name, sources, refs, defines, out_dir, extra=()) -> bool: |
| 63 | + out = out_dir / f"{re.sub(r'[^A-Za-z0-9_.]', '_', name)}.dll" |
| 64 | + rsp = out_dir / f"{out.stem}.rsp" |
| 65 | + args = [ |
| 66 | + "-nologo", "-noconfig", "-nostdlib+", "-target:library", "-deterministic", |
| 67 | + f"-langversion:{LANG_VERSION}", f"-nowarn:{NOWARN}", f"-out:{out}", |
| 68 | + "-define:" + ";".join(defines), |
| 69 | + *extra, |
| 70 | + *[f"-r:{r}" for r in refs], |
| 71 | + *[str(s) for s in sources], |
| 72 | + ] |
| 73 | + rsp.write_text("\n".join(f'"{a}"' if " " in a else a for a in args), encoding="utf-8") |
| 74 | + result = subprocess.run([*csc, f"@{rsp}"], capture_output=True, text=True) |
| 75 | + if result.returncode != 0: |
| 76 | + print(f"FAIL {name}") |
| 77 | + for line in (result.stdout + result.stderr).splitlines(): |
| 78 | + if "error" in line: |
| 79 | + print(" " + line.replace(str(ROOT) + os.sep, "")) |
| 80 | + return False |
| 81 | + print(f"ok {name}") |
| 82 | + return True |
| 83 | + |
| 84 | + |
| 85 | +def package_sources(package_root: Path): |
| 86 | + """C# files of the package's main assembly, skipping nested assemblies and tests.""" |
| 87 | + asm_root = package_root / "InputSystem" |
| 88 | + nested = {p.parent for p in asm_root.rglob("*.asmdef") if p.parent != asm_root} |
| 89 | + return sorted( |
| 90 | + p for p in asm_root.rglob("*.cs") |
| 91 | + if not any(n in p.parents for n in nested) |
| 92 | + ) |
| 93 | + |
| 94 | + |
| 95 | +def main() -> int: |
| 96 | + parser = argparse.ArgumentParser() |
| 97 | + parser.add_argument("--unity", required=True, type=Path, help="Unity Editor/Data folder") |
| 98 | + parser.add_argument("--inputsystem", required=True, type=Path, help="com.unity.inputsystem package folder") |
| 99 | + args = parser.parse_args() |
| 100 | + |
| 101 | + modules = args.unity / "Managed" / "UnityEngine" |
| 102 | + netstandard = args.unity / "NetStandard" / "ref" / "2.1.0" / "netstandard.dll" |
| 103 | + if not modules.is_dir() or not netstandard.is_file(): |
| 104 | + sys.exit(f"Unity reference assemblies not found under {args.unity}") |
| 105 | + |
| 106 | + engine_refs = [netstandard, *sorted(modules.glob("UnityEngine*.dll"))] |
| 107 | + editor_refs = [*engine_refs, *sorted(modules.glob("UnityEditor*.dll"))] |
| 108 | + |
| 109 | + csc = find_csc() |
| 110 | + units = [(f"snippets/csharp/{p.name}", [p]) for p in sorted((ROOT / "snippets" / "csharp").glob("*.cs"))] |
| 111 | + units += [ |
| 112 | + (f"templates/{d.name}", sorted(d.glob("*.cs"))) |
| 113 | + for d in sorted((ROOT / "templates").iterdir()) |
| 114 | + if d.is_dir() and any(d.glob("*.cs")) |
| 115 | + ] |
| 116 | + |
| 117 | + ok = True |
| 118 | + with tempfile.TemporaryDirectory() as tmp: |
| 119 | + tmp = Path(tmp) |
| 120 | + passes = { |
| 121 | + "player": (engine_refs, UNITY_DEFINES), |
| 122 | + "editor": (editor_refs, [*UNITY_DEFINES, "UNITY_EDITOR", "DEBUG", "TRACE"]), |
| 123 | + } |
| 124 | + for pass_name, (refs, defines) in passes.items(): |
| 125 | + out_dir = tmp / pass_name |
| 126 | + out_dir.mkdir() |
| 127 | + print(f"== {pass_name} pass") |
| 128 | + |
| 129 | + # Build the Input System package as a reference, like Unity would |
| 130 | + input_dll = out_dir / "Unity.InputSystem.dll" |
| 131 | + if not compile_unit( |
| 132 | + csc, "Unity.InputSystem", package_sources(args.inputsystem), refs, |
| 133 | + [*defines, "UNITY_INPUT_SYSTEM_ENABLE_PHYSICS", "UNITY_INPUT_SYSTEM_ENABLE_PHYSICS2D"], |
| 134 | + out_dir, extra=["-unsafe+", "-nowarn:0618,0108,0114,0162,0219,0414,0618,0672,1717"], |
| 135 | + ): |
| 136 | + return 1 |
| 137 | + |
| 138 | + for name, sources in units: |
| 139 | + ok &= compile_unit( |
| 140 | + csc, name, sources, [*refs, input_dll], defines, out_dir, |
| 141 | + extra=["-warnaserror+:CS0618"], |
| 142 | + ) |
| 143 | + return 0 if ok else 1 |
| 144 | + |
| 145 | + |
| 146 | +if __name__ == "__main__": |
| 147 | + sys.exit(main()) |
0 commit comments