unity-devkit 0.1.15__tar.gz → 0.1.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. unity_devkit-0.1.17/.gitignore +22 -0
  2. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/PKG-INFO +1 -3
  3. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/pyproject.toml +6 -22
  4. unity_devkit-0.1.17/src/unity_devkit/build_unity.py +41 -0
  5. unity_devkit-0.1.15/src/unity_devkit/build_unity.py → unity_devkit-0.1.17/src/unity_devkit/ci_build_unity.py +23 -27
  6. unity_devkit-0.1.15/src/unity_devkit/check_unity.py → unity_devkit-0.1.17/src/unity_devkit/compile_check_unity.py +3 -8
  7. unity_devkit-0.1.17/src/unity_devkit/dispatch_workflow_generator.py +258 -0
  8. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/install.py +10 -32
  9. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/lock_unity.py +1 -1
  10. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/matrix.py +10 -10
  11. unity_devkit-0.1.15/src/unity_devkit/unity.py → unity_devkit-0.1.17/src/unity_devkit/player_build.py +124 -52
  12. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/projects.py +1 -5
  13. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/test_unity.py +1 -1
  14. unity_devkit-0.1.17/tests/fixtures/environment-dump.json +63 -0
  15. unity_devkit-0.1.17/tests/test_build_player.py +156 -0
  16. unity_devkit-0.1.17/tests/test_dispatch_workflow_generator.py +210 -0
  17. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/tests/test_matrix.py +6 -8
  18. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/tests/test_prepare.py +6 -6
  19. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/tests/test_projects.py +2 -4
  20. unity_devkit-0.1.17/tests/test_run_unity_batchmode.py +54 -0
  21. unity_devkit-0.1.15/.csharpierrc.json +0 -6
  22. unity_devkit-0.1.15/.gitattributes +0 -1
  23. unity_devkit-0.1.15/.github/actions/unity-container-prep/action.yml +0 -37
  24. unity_devkit-0.1.15/.github/workflows/ci.yml +0 -51
  25. unity_devkit-0.1.15/.github/workflows/release.yml +0 -35
  26. unity_devkit-0.1.15/.github/workflows/unity-build.yml +0 -97
  27. unity_devkit-0.1.15/.github/workflows/unity-check.yml +0 -58
  28. unity_devkit-0.1.15/.github/workflows/unity-prepare.yml +0 -90
  29. unity_devkit-0.1.15/.gitignore +0 -22
  30. unity_devkit-0.1.15/AGENTS.md +0 -57
  31. unity_devkit-0.1.15/CLAUDE.md +0 -1
  32. unity_devkit-0.1.15/LICENSE +0 -201
  33. unity_devkit-0.1.15/NOTICE +0 -1
  34. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/BuildConfig.cs +0 -118
  35. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/BuildConfig.cs.meta +0 -2
  36. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/ConfigureWindow.cs +0 -238
  37. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/ConfigureWindow.cs.meta +0 -2
  38. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/Outernet.PlayerBuild.Editor.asmdef +0 -22
  39. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/Outernet.PlayerBuild.Editor.asmdef.meta +0 -7
  40. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/PlayerBuild.cs +0 -670
  41. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/PlayerBuild.cs.meta +0 -2
  42. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/SerializableBuildReport.cs +0 -76
  43. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/SerializableBuildReport.cs.meta +0 -2
  44. unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor.meta +0 -8
  45. unity_devkit-0.1.15/PlayerBuild/Assets/Package/package.json +0 -13
  46. unity_devkit-0.1.15/PlayerBuild/Assets/Package/package.json.meta +0 -7
  47. unity_devkit-0.1.15/PlayerBuild/Assets/Package.meta +0 -8
  48. unity_devkit-0.1.15/PlayerBuild/Packages/manifest.json +0 -8
  49. unity_devkit-0.1.15/PlayerBuild/Packages/packages-lock.json +0 -165
  50. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/AudioManager.asset +0 -20
  51. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/ClusterInputManager.asset +0 -6
  52. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/DynamicsManager.asset +0 -44
  53. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/EditorBuildSettings.asset +0 -9
  54. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/EditorSettings.asset +0 -50
  55. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/GraphicsSettings.asset +0 -66
  56. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/InputManager.asset +0 -296
  57. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/MemorySettings.asset +0 -35
  58. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/MultiplayerManager.asset +0 -7
  59. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/NavMeshAreas.asset +0 -93
  60. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/Physics2DSettings.asset +0 -56
  61. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/PresetManager.asset +0 -7
  62. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/ProjectSettings.asset +0 -785
  63. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/ProjectVersion.txt +0 -2
  64. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/QualitySettings.asset +0 -341
  65. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/TagManager.asset +0 -45
  66. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/TimeManager.asset +0 -14
  67. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/UnityConnectSettings.asset +0 -36
  68. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/VFXManager.asset +0 -19
  69. unity_devkit-0.1.15/PlayerBuild/ProjectSettings/VersionControlSettings.asset +0 -7
  70. unity_devkit-0.1.15/README.md +0 -51
  71. unity_devkit-0.1.15/release-devkit.json +0 -21
  72. unity_devkit-0.1.15/ruff.base.toml +0 -204
  73. unity_devkit-0.1.15/ruff.toml +0 -5
  74. unity_devkit-0.1.15/src/unity_devkit/compile_unity.py +0 -58
  75. unity_devkit-0.1.15/src/unity_devkit/versioning.py +0 -37
  76. unity_devkit-0.1.15/tests/test_latest_tag_version.py +0 -19
  77. unity_devkit-0.1.15/tests/test_resolve.py +0 -56
  78. unity_devkit-0.1.15/tests/test_run_unity_batchmode.py +0 -53
  79. unity_devkit-0.1.15/tests/test_versioning.py +0 -66
  80. unity_devkit-0.1.15/unity-devkit.json +0 -5
  81. unity_devkit-0.1.15/uv.lock +0 -366
  82. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/__init__.py +0 -0
  83. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/license.py +0 -0
  84. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/license_restore.py +0 -0
  85. {unity_devkit-0.1.15 → unity_devkit-0.1.17}/src/unity_devkit/py.typed +0 -0
@@ -0,0 +1,22 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .ruff_cache/
5
+ .pytest_cache/
6
+ bookmark.md
7
+
8
+ # PlayerBuild Unity harness
9
+ packages/unity/PlayerBuild/*.csproj
10
+ packages/unity/PlayerBuild/*.sln
11
+ packages/unity/PlayerBuild/Assets/Packages/
12
+ packages/unity/PlayerBuild/Assets/_LocalWorkspace
13
+ packages/unity/PlayerBuild/Assets/_LocalWorkspace.meta
14
+ packages/unity/PlayerBuild/Build/
15
+ packages/unity/PlayerBuild/Build.meta
16
+ packages/unity/PlayerBuild/Builds/
17
+ packages/unity/PlayerBuild/Library/
18
+ packages/unity/PlayerBuild/Logs/
19
+ packages/unity/PlayerBuild/Temp/
20
+ packages/unity/PlayerBuild/UserSettings/
21
+ packages/unity/PlayerBuild/obj/
22
+ packages/unity/PlayerBuild/.utmp/
@@ -1,9 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unity-devkit
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: Unity build, license, and CI tooling
5
- License-File: LICENSE
6
- License-File: NOTICE
7
5
  Requires-Python: >=3.13
8
6
  Requires-Dist: bashrun>=0.1.0
9
7
  Requires-Dist: ci-devkit>=0.1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unity-devkit"
3
- version = "0.1.15"
3
+ version = "0.1.17"
4
4
  description = "Unity build, license, and CI tooling"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
@@ -11,20 +11,18 @@ dependencies = [
11
11
  "pydantic-settings>=2.9.1",
12
12
  ]
13
13
 
14
- [dependency-groups]
15
- dev = ["basedpyright>=1.39.10", "ruff>=0.14.11", "pytest>=8.0.0"]
16
-
17
14
  [project.scripts]
18
15
  activate-unity-license = "unity_devkit.license:activate_app"
19
16
  build-unity = "unity_devkit.build_unity:app"
20
- check-unity = "unity_devkit.check_unity:app"
21
- compile-unity = "unity_devkit.compile_unity:app"
17
+ build-unity-matrix = "unity_devkit.matrix:build_matrix"
18
+ ci-build-unity = "unity_devkit.ci_build_unity:app"
19
+ compile-check-unity = "unity_devkit.compile_check_unity:app"
20
+ compile-check-unity-matrix = "unity_devkit.matrix:compile_check_matrix"
22
21
  install = "unity_devkit.install:app"
23
22
  lock-unity = "unity_devkit.lock_unity:app"
24
23
  test-unity = "unity_devkit.test_unity:app"
25
24
  unity-license-tag = "unity_devkit.license_restore:tag_app"
26
- unity-check-matrix = "unity_devkit.matrix:check_matrix"
27
- unity-matrix = "unity_devkit.matrix:main"
25
+ unity-dispatch = "unity_devkit.dispatch_workflow_generator:app"
28
26
 
29
27
  [build-system]
30
28
  requires = ["hatchling"]
@@ -33,17 +31,3 @@ build-backend = "hatchling.build"
33
31
  [tool.hatch.build.targets.wheel]
34
32
  packages = ["src/unity_devkit"]
35
33
  include = ["src/unity_devkit/py.typed"]
36
-
37
- [tool.uv.sources]
38
-
39
- [tool.basedpyright]
40
- venvPath = "."
41
- venv = ".venv"
42
- typeCheckingMode = "strict"
43
- include = ["src", "tests"]
44
-
45
- [tool.pytest.ini_options]
46
- testpaths = ["tests"]
47
-
48
- [tool.python-devkit.preflight]
49
- tests = true
@@ -0,0 +1,41 @@
1
+ from typing import Annotated
2
+
3
+ import typer
4
+
5
+ from .player_build import build_player, parse_environment_fields
6
+
7
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
8
+
9
+
10
+ @app.command()
11
+ def build_unity(
12
+ project: Annotated[str, typer.Option(help="Unity project name (catalog key in unity-devkit.json)")],
13
+ build: Annotated[str, typer.Option(help="Build target from the project's builds list (e.g. AndroidMobile)")],
14
+ version: Annotated[
15
+ str,
16
+ typer.Option(help="Version string to stamp into ProjectSettings.asset; empty builds unversioned"),
17
+ ] = "",
18
+ run_number: Annotated[int, typer.Option(help="bundleVersionCode to stamp; local builds default to 0")] = 0,
19
+ development: Annotated[bool, typer.Option(help="Development build column")] = False,
20
+ environment_preset: Annotated[
21
+ str,
22
+ typer.Option(
23
+ help="Environment preset name (the env class's Presets key); unset leaves the workspace untouched"
24
+ ),
25
+ ] = "",
26
+ environment_field: Annotated[
27
+ list[str] | None,
28
+ typer.Option("--environment-field", help="Environment field override as path=value (repeatable)"),
29
+ ] = None,
30
+ ) -> None:
31
+ fields = parse_environment_fields(environment_field or [])
32
+ for artifact in build_player(
33
+ project,
34
+ build,
35
+ version=version,
36
+ run_number=run_number,
37
+ development=development,
38
+ environment_preset=environment_preset,
39
+ environment_fields=fields,
40
+ ):
41
+ print(f"Built: {artifact}")
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  import shutil
5
4
  from pathlib import Path
6
5
  from typing import Annotated
@@ -13,8 +12,7 @@ from ci_devkit.ci_step import ci_step
13
12
  from .license_restore import restore_license
14
13
  from ci_devkit.setup import configure_git, install_dotnet
15
14
  from ci_devkit.setup_oras import install_oras
16
- from .unity import prepare_unity_project, resolve_unity_build, run_unity_batchmode
17
- from .versioning import stamp_build_version
15
+ from .player_build import build_player, parse_environment_fields, resolve_unity_project
18
16
 
19
17
 
20
18
  class Settings(BaseSettings):
@@ -25,7 +23,7 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
25
23
 
26
24
 
27
25
  @app.command()
28
- def main(
26
+ def ci_build_unity(
29
27
  project: Annotated[str, typer.Option(help="Project name")],
30
28
  project_path: Annotated[Path, typer.Option(help="Path to Unity project")],
31
29
  platform: Annotated[str, typer.Option(help="Target platform")],
@@ -33,20 +31,19 @@ def main(
33
31
  registry: Annotated[str, typer.Option(help="OCI registry path")],
34
32
  run_number: Annotated[int, typer.Option(help="CI run number")] = 0,
35
33
  branch: Annotated[str, typer.Option(help="Git branch name")] = "dev",
36
- build_env: Annotated[
37
- str, typer.Option(help="Newline-separated KEY=VALUE pairs injected into the Unity build process environment")
34
+ environment_preset: Annotated[
35
+ str, typer.Option(help="Environment preset name; empty leaves the workspace untouched")
36
+ ] = "",
37
+ development: Annotated[bool, typer.Option("--development", help="Development build column")] = False,
38
+ environment_fields: Annotated[
39
+ str, typer.Option(help="Newline-separated path=value environment field overrides")
40
+ ] = "",
41
+ version: Annotated[
42
+ str, typer.Option(help="Version string to stamp into ProjectSettings.asset; empty builds unversioned")
38
43
  ] = "",
39
44
  ) -> None:
40
45
  settings = Settings.model_validate({})
41
-
42
- for line in build_env.splitlines():
43
- entry = line.strip()
44
- if not entry:
45
- continue
46
- key, separator, value = entry.partition("=")
47
- if not separator:
48
- raise SystemExit(f"Invalid --build-env entry (expected KEY=VALUE): {entry!r}")
49
- os.environ[key.strip()] = value.strip()
46
+ fields = parse_environment_fields(environment_fields.splitlines())
50
47
 
51
48
  with ci_step("Setup"):
52
49
  configure_git(settings.github_workspace)
@@ -63,23 +60,22 @@ def main(
63
60
  restore(registry, "unity-library", tag, Path("."), fallback_tags=fallback_tags)
64
61
 
65
62
  with ci_step("Prepare build"):
66
- project_config, build_flag, execute_method = resolve_unity_build(project, platform)
67
- unity_project_path = project_config.path
63
+ unity_project_path = resolve_unity_project(project).path
68
64
  if unity_project_path.resolve() != project_path.resolve():
69
65
  raise SystemExit(
70
66
  f"--project-path {project_path} does not match catalog entry '{project}' at {unity_project_path}"
71
67
  )
72
68
 
73
- with ci_step("Prepare project"):
74
- prepare_unity_project(unity_project_path)
75
-
76
69
  with ci_step(f"Build {project} [{platform}]"):
77
- tag_prefix = project_config.tag_prefix
78
- if tag_prefix:
79
- full_version = stamp_build_version(unity_project_path, tag_prefix, run_number, release=(branch == "main"))
80
- print(f"Stamped bundleVersion {full_version} (bundleVersionCode={run_number}) into ProjectSettings")
81
-
82
- run_unity_batchmode(unity_project_path, f"{build_flag} -executeMethod {execute_method}", nographics=False)
70
+ build_player(
71
+ project,
72
+ platform,
73
+ version=version,
74
+ run_number=run_number,
75
+ development=development,
76
+ environment_preset=environment_preset,
77
+ environment_fields=fields,
78
+ )
83
79
 
84
80
  with ci_step("Save library cache"):
85
81
  # PackageCache (~1.6 GiB) is redundant with the shared UPM cache at ~/.cache/Unity/upm/
@@ -94,7 +90,7 @@ def main(
94
90
  if build_directory.is_dir():
95
91
  artifact_directory = Path("/tmp/unity-builds")
96
92
  artifact_directory.mkdir(parents=True, exist_ok=True)
97
- if platform == "linux64":
93
+ if platform == "Linux":
98
94
  shutil.copytree(build_directory, artifact_directory, dirs_exist_ok=True)
99
95
  else:
100
96
  for file in build_directory.rglob("*"):
@@ -2,8 +2,7 @@ from typing import Annotated
2
2
 
3
3
  import typer
4
4
 
5
- from .projects import load_catalog
6
- from .unity import prepare_unity_project, run_unity_batchmode
5
+ from .player_build import prepare_unity_project, resolve_unity_project, run_unity_batchmode
7
6
 
8
7
  app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
9
8
 
@@ -11,7 +10,7 @@ COMPILE_ERROR_SIGNATURES = ("error CS",)
11
10
 
12
11
 
13
12
  @app.command()
14
- def check_unity(
13
+ def compile_check_unity(
15
14
  project: Annotated[str, typer.Option(help="Unity project name (catalog key in unity-devkit.json)")],
16
15
  execute_method: Annotated[
17
16
  str | None,
@@ -22,11 +21,7 @@ def check_unity(
22
21
  typer.Option(help="Startup build target (e.g. Android) for sessions that must open on a non-default platform"),
23
22
  ] = None,
24
23
  ) -> None:
25
- projects = load_catalog()
26
- if project not in projects:
27
- raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
28
-
29
- project_path = projects[project].path
24
+ project_path = resolve_unity_project(project).path
30
25
  extra_flags = ""
31
26
  if build_target:
32
27
  extra_flags += f" -buildTarget {build_target}"
@@ -0,0 +1,258 @@
1
+ import json
2
+ from collections import Counter
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+ from .compile_check_unity import COMPILE_ERROR_SIGNATURES
10
+ from .projects import load_catalog
11
+ from .player_build import prepare_unity_project, run_unity_batchmode
12
+
13
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
14
+
15
+ DUMP_EXECUTE_METHOD = "Outernet.PlayerBuild.DumpEnvironment"
16
+ DISPATCH_INPUT_CAP = 25
17
+ RESERVED_INPUT_IDS = frozenset({"environment-preset", "development"})
18
+ NUMERIC_FIELD_TYPES = {
19
+ "Byte",
20
+ "SByte",
21
+ "Int16",
22
+ "UInt16",
23
+ "Int32",
24
+ "UInt32",
25
+ "Int64",
26
+ "UInt64",
27
+ "Single",
28
+ "Double",
29
+ "Decimal",
30
+ }
31
+
32
+
33
+ class EnvironmentEnum(BaseModel):
34
+ model_config = ConfigDict(extra="forbid")
35
+
36
+ name: str
37
+ values: list[str]
38
+ flags: bool
39
+
40
+
41
+ class EnvironmentFieldDump(BaseModel):
42
+ model_config = ConfigDict(extra="forbid")
43
+
44
+ path: str
45
+ field_type: str
46
+
47
+
48
+ class EnvironmentDump(BaseModel):
49
+ model_config = ConfigDict(extra="forbid")
50
+
51
+ class_name: str
52
+ mode_field: str
53
+ target_path: str
54
+ presets: dict[str, str]
55
+ enums: list[EnvironmentEnum]
56
+ fields: list[EnvironmentFieldDump]
57
+
58
+
59
+ @app.command()
60
+ def unity_dispatch(
61
+ project: Annotated[str, typer.Option(help="Unity project name (catalog key in unity-devkit.json)")],
62
+ output: Annotated[Path, typer.Option(help="Workflow file to write (or check with --check)")],
63
+ build_workflow: Annotated[
64
+ str,
65
+ typer.Option(
66
+ help="The build job's uses: value — the consumer's pinned build-unity.yml reference "
67
+ "(owner/repo/.github/workflows/build-unity.yml@sha, or ./.github/workflows/build-unity.yml inside unity-devkit)"
68
+ ),
69
+ ],
70
+ environment_config_class: Annotated[
71
+ str | None,
72
+ typer.Option(help="Env class file path override (defaults to build-config.json's environment_config)"),
73
+ ] = None,
74
+ check: Annotated[
75
+ bool, typer.Option("--check", help="Compare against the existing file instead of writing; fail on drift")
76
+ ] = False,
77
+ ) -> None:
78
+ projects = load_catalog()
79
+ if project not in projects:
80
+ raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
81
+
82
+ project_path = projects[project].path
83
+ env = {"ENVIRONMENT_CONFIG_CLASS": environment_config_class} if environment_config_class else None
84
+ print(f"Dumping environment of {project}...")
85
+ prepare_unity_project(project_path)
86
+ log_path = run_unity_batchmode(
87
+ project_path,
88
+ f"-executeMethod {DUMP_EXECUTE_METHOD}",
89
+ extra_failure_signatures=COMPILE_ERROR_SIGNATURES,
90
+ env=env,
91
+ )
92
+ dump = extract_environment_dump(log_path)
93
+ workflow = render_dispatch_workflow(dump, project=project, output=output.as_posix(), build_workflow=build_workflow)
94
+
95
+ if check:
96
+ if not output.is_file():
97
+ raise SystemExit(f"Dispatch workflow drift: {output} does not exist — generate it")
98
+ committed = output.read_text(encoding="utf-8")
99
+ if committed != workflow:
100
+ raise SystemExit(
101
+ f"Dispatch workflow drift: {output} differs from the environment class — regenerate with "
102
+ f"'{regen_command(project, output.as_posix(), build_workflow)}'"
103
+ )
104
+ print(f" {output} matches the environment class")
105
+ return
106
+
107
+ output.parent.mkdir(parents=True, exist_ok=True)
108
+ output.write_text(workflow, encoding="utf-8")
109
+ print(f" wrote {output}")
110
+
111
+
112
+ def extract_environment_dump(log_path: Path) -> EnvironmentDump:
113
+ for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines():
114
+ candidate = line.strip()
115
+ if not candidate.startswith("{"):
116
+ continue
117
+ try:
118
+ payload = json.loads(candidate)
119
+ except json.JSONDecodeError:
120
+ continue
121
+ if isinstance(payload, dict) and "class_name" in payload:
122
+ return EnvironmentDump.model_validate(payload)
123
+
124
+ raise SystemExit(f"No environment dump JSON found in the editor log at {log_path}")
125
+
126
+
127
+ def render_dispatch_workflow(dump: EnvironmentDump, *, project: str, output: str, build_workflow: str) -> str:
128
+ enums_by_name = {environment_enum.name: environment_enum for environment_enum in dump.enums}
129
+ emitted_fields = [field for field in dump.fields if field.path != dump.mode_field]
130
+ leaf_counts = Counter(field.path.rsplit(".", 1)[-1] for field in emitted_fields)
131
+ input_ids: dict[str, str] = {}
132
+ for field in emitted_fields:
133
+ leaf = field.path.rsplit(".", 1)[-1]
134
+ input_ids[field.path] = (
135
+ leaf if leaf_counts[leaf] == 1 and leaf not in RESERVED_INPUT_IDS else field.path.replace(".", "-")
136
+ )
137
+ default_preset = next(iter(dump.presets), "")
138
+ input_blocks: list[tuple[str, str | None, list[str], str | None]] = [
139
+ (
140
+ "environment-preset",
141
+ "Preset to apply — blank leaves the workspace untouched; leave fields empty to keep the preset's values",
142
+ ["type: choice", "options:", *indent_lines([*(f'- "{preset}"' for preset in ("", *dump.presets))], 2)],
143
+ f'default: "{default_preset}"',
144
+ ),
145
+ ("development", "Development build (debug symbols)", ["type: boolean"], "default: false"),
146
+ ]
147
+ for field in emitted_fields:
148
+ input_blocks.append(field_input_block(field, input_ids[field.path], enums_by_name))
149
+
150
+ input_count = len(input_blocks)
151
+ if input_count > DISPATCH_INPUT_CAP:
152
+ raise SystemExit(
153
+ f"Dispatch panel would carry {input_count} inputs — GitHub's workflow_dispatch cap is {DISPATCH_INPUT_CAP}. "
154
+ "Shrink the env class or move configuration out of it."
155
+ )
156
+
157
+ transport_lines = [field_transport_line(field, input_ids[field.path]) for field in emitted_fields]
158
+ header = (
159
+ f"# Generated by '{regen_command(project, output, build_workflow)}' from the environment class "
160
+ f"{dump.class_name} —"
161
+ )
162
+
163
+ lines = [
164
+ header,
165
+ "# edit the class, then regenerate this workflow.",
166
+ "name: Unity Dispatch Build",
167
+ "",
168
+ "on:",
169
+ " workflow_dispatch:",
170
+ " inputs:",
171
+ *indent_lines([line for block in input_blocks for line in build_input_block(block)], 6),
172
+ "",
173
+ "jobs:",
174
+ " build:",
175
+ f" uses: {build_workflow}",
176
+ " with:",
177
+ " environment-preset: ${{ inputs['environment-preset'] }}",
178
+ " development: ${{ inputs.development }}",
179
+ ]
180
+ if transport_lines:
181
+ lines.append(" environment-fields: |-")
182
+ lines.extend(indent_lines(transport_lines, 8))
183
+ else:
184
+ lines.append(" environment-fields: ''")
185
+ lines.append(" secrets: inherit")
186
+ return "\n".join(lines) + "\n"
187
+
188
+
189
+ def regen_command(project: str, output: str, build_workflow: str) -> str:
190
+ return f"uv run unity-dispatch --project {project} --output {output} --build-workflow {build_workflow}"
191
+
192
+
193
+ def field_input_block(
194
+ field: EnvironmentFieldDump, input_id: str, enums_by_name: dict[str, EnvironmentEnum]
195
+ ) -> tuple[str, str | None, list[str], str | None]:
196
+ if field.field_type == "Boolean":
197
+ return (input_id, None, ["type: boolean"], "default: false")
198
+
199
+ if field.field_type in enums_by_name:
200
+ environment_enum = enums_by_name[field.field_type]
201
+ if environment_enum.flags:
202
+ return (
203
+ input_id,
204
+ f"comma-separated: {', '.join(environment_enum.values)}",
205
+ ["type: string"],
206
+ 'default: ""',
207
+ )
208
+ return (
209
+ input_id,
210
+ None,
211
+ [
212
+ "type: choice",
213
+ "options:",
214
+ *indent_lines([*(f'- "{value}"' for value in ("", *environment_enum.values))], 2),
215
+ ],
216
+ 'default: ""',
217
+ )
218
+
219
+ if field.field_type == "String":
220
+ return (input_id, None, ["type: string"], 'default: ""')
221
+
222
+ if field.field_type in NUMERIC_FIELD_TYPES:
223
+ return (input_id, None, ["type: number"], None)
224
+
225
+ raise SystemExit(
226
+ f"Environment field '{field.path}' has type '{field.field_type}' — no dispatch input type maps to it "
227
+ "(arrays and object references self-exclude from the dump; this is contract drift)"
228
+ )
229
+
230
+
231
+ def field_transport_line(field: EnvironmentFieldDump, input_id: str) -> str:
232
+ guard = "" if field.field_type == "Boolean" else " != ''"
233
+ return (
234
+ "${{ inputs['"
235
+ + input_id
236
+ + "']"
237
+ + guard
238
+ + " && format('"
239
+ + field.path
240
+ + "={0}', inputs['"
241
+ + input_id
242
+ + "']) || '' }}"
243
+ )
244
+
245
+
246
+ def build_input_block(input_block: tuple[str, str | None, list[str], str | None]) -> list[str]:
247
+ input_id, description, type_lines, default_line = input_block
248
+ lines = [f"{input_id}:"]
249
+ if description is not None:
250
+ lines.append(f' description: "{description}"')
251
+ lines.extend(f" {line}" for line in type_lines)
252
+ if default_line is not None:
253
+ lines.append(f" {default_line}")
254
+ return lines
255
+
256
+
257
+ def indent_lines(lines: list[str], spaces: int) -> list[str]:
258
+ return [f"{' ' * spaces}{line}" if line else line for line in lines]
@@ -6,13 +6,13 @@ from pathlib import Path
6
6
  from typing import Annotated, Any
7
7
 
8
8
  import typer
9
- from bashrun.bash import bash, bash_check, bash_handoff, bash_output
9
+ from bashrun.bash import bash, bash_handoff, bash_output
10
10
 
11
- from .compile_unity import build_unity_project
12
11
  from .projects import load_catalog
12
+ from .player_build import build_player
13
13
 
14
- INSTALLABLE_TARGETS = {"android-mobile", "magicleap", "linux64"}
15
- ADB_TARGETS = {"android-mobile", "magicleap"}
14
+ INSTALLABLE_TARGETS = {"AndroidMobile", "MagicLeap2", "Linux"}
15
+ ADB_TARGETS = {"AndroidMobile", "MagicLeap2"}
16
16
  CACHE_ROOT = Path.home() / ".unity-devkit" / "builds"
17
17
 
18
18
  app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
@@ -23,7 +23,7 @@ def main(
23
23
  project: Annotated[str, typer.Option("--project", "-p", help="Unity project name")],
24
24
  target: Annotated[
25
25
  str | None,
26
- typer.Option("--target", "-t", help="Device target (android-mobile, magicleap, linux64)"),
26
+ typer.Option("--target", "-t", help="Device target (AndroidMobile, MagicLeap2, Linux)"),
27
27
  ] = None,
28
28
  branch: Annotated[
29
29
  str | None,
@@ -31,23 +31,13 @@ def main(
31
31
  ] = None,
32
32
  run: Annotated[int | None, typer.Option("--run", "-r", help="Specific GitHub Actions run ID")] = None,
33
33
  serial: Annotated[str | None, typer.Option("--serial", "-s", help="adb device serial")] = None,
34
- no_grant_permissions: Annotated[
35
- bool,
36
- typer.Option(
37
- "--no-grant-permissions",
38
- help=(
39
- "Skip the post-install `adb shell pm grant` calls listed under `grant_permissions` "
40
- "for the project's entry in unity-devkit.json. Permissions are granted by default."
41
- ),
42
- ),
43
- ] = False,
44
34
  build_locally: Annotated[
45
35
  bool,
46
36
  typer.Option(
47
37
  "--build",
48
38
  "-B",
49
39
  help=(
50
- "Compile the project locally via `compile-unity` and install the produced APK / "
40
+ "Build the project locally via `build-unity` and install the produced APK / "
51
41
  "linux executable. Skips the GitHub Actions artifact lookup; --branch / --run are ignored."
52
42
  ),
53
43
  ),
@@ -81,9 +71,9 @@ def main(
81
71
  if build_locally:
82
72
  if branch or run:
83
73
  print("Warning: --branch / --run are ignored when --build is set")
84
- produced = build_unity_project(project_name, target_name)
74
+ produced = build_player(project_name, target_name)
85
75
  apks = [path for path in produced if path.suffix == ".apk"]
86
- executables = [path for path in produced if path.suffix == ".exe"]
76
+ executables = [path for path in produced if path.suffix in {".exe", ".x86_64"}]
87
77
  else:
88
78
  artifact_name = f"{project_name}-{target_name}"
89
79
 
@@ -127,7 +117,7 @@ def main(
127
117
  apks = sorted(cache_path.rglob("*.apk"))
128
118
 
129
119
  executables = []
130
- if target_name == "linux64":
120
+ if target_name == "Linux":
131
121
  executable = next(
132
122
  (
133
123
  item
@@ -137,7 +127,7 @@ def main(
137
127
  None,
138
128
  )
139
129
  if executable is None:
140
- print("No linux64 executable found in artifact (expected a file with a matching _Data/ directory)")
130
+ print("No Linux executable found in artifact (expected a file with a matching _Data/ directory)")
141
131
  raise SystemExit(1)
142
132
  executables = [executable]
143
133
 
@@ -148,19 +138,7 @@ def main(
148
138
 
149
139
  print(f"Installing: {apks[0].name}")
150
140
  adb_prefix = f"adb -s {serial}" if serial else "adb"
151
- package = project_config.package
152
- if package:
153
- bash_check(f"{adb_prefix} uninstall {package}")
154
141
  bash(f"{adb_prefix} install {apks[0]}")
155
- permissions = project_config.grant_permissions
156
- if permissions and not no_grant_permissions:
157
- if not package:
158
- raise typer.BadParameter(
159
- f"{project_name} has grant_permissions but no 'package' field in its unity-devkit.json entry"
160
- )
161
- for permission in permissions:
162
- print(f"Granting {permission} to {package}")
163
- bash(f"{adb_prefix} shell pm grant {package} {permission}")
164
142
  print("Done.")
165
143
  else:
166
144
  if not executables:
@@ -3,7 +3,7 @@ from typing import Annotated
3
3
  import typer
4
4
 
5
5
  from .projects import load_catalog
6
- from .unity import prepare_unity_project, run_unity_batchmode
6
+ from .player_build import prepare_unity_project, run_unity_batchmode
7
7
 
8
8
  app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
9
9