unity-devkit 0.1.14__tar.gz → 0.1.16__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.
- unity_devkit-0.1.16/.gitignore +22 -0
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/PKG-INFO +1 -3
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/pyproject.toml +6 -20
- unity_devkit-0.1.16/src/unity_devkit/build_unity.py +41 -0
- unity_devkit-0.1.14/src/unity_devkit/build_unity.py → unity_devkit-0.1.16/src/unity_devkit/ci_build_unity.py +23 -38
- unity_devkit-0.1.16/src/unity_devkit/compile_check_unity.py +33 -0
- unity_devkit-0.1.16/src/unity_devkit/dispatch_workflow_generator.py +266 -0
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/src/unity_devkit/install.py +10 -32
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/src/unity_devkit/lock_unity.py +1 -1
- unity_devkit-0.1.16/src/unity_devkit/matrix.py +52 -0
- unity_devkit-0.1.14/src/unity_devkit/unity.py → unity_devkit-0.1.16/src/unity_devkit/player_build.py +129 -54
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/src/unity_devkit/projects.py +1 -5
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/src/unity_devkit/test_unity.py +1 -1
- unity_devkit-0.1.16/tests/fixtures/environment-dump.json +63 -0
- unity_devkit-0.1.16/tests/test_build_player.py +156 -0
- unity_devkit-0.1.16/tests/test_dispatch_workflow_generator.py +160 -0
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/tests/test_matrix.py +19 -7
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/tests/test_prepare.py +6 -6
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/tests/test_projects.py +2 -4
- unity_devkit-0.1.16/tests/test_run_unity_batchmode.py +54 -0
- unity_devkit-0.1.14/.gitattributes +0 -1
- unity_devkit-0.1.14/.github/workflows/ci.yml +0 -26
- unity_devkit-0.1.14/.github/workflows/release.yml +0 -31
- unity_devkit-0.1.14/.github/workflows/unity-build.yml +0 -146
- unity_devkit-0.1.14/.gitignore +0 -6
- unity_devkit-0.1.14/AGENTS.md +0 -51
- unity_devkit-0.1.14/CLAUDE.md +0 -1
- unity_devkit-0.1.14/LICENSE +0 -201
- unity_devkit-0.1.14/NOTICE +0 -1
- unity_devkit-0.1.14/README.md +0 -48
- unity_devkit-0.1.14/publish-config.json +0 -11
- unity_devkit-0.1.14/ruff.base.toml +0 -204
- unity_devkit-0.1.14/ruff.toml +0 -5
- unity_devkit-0.1.14/src/unity_devkit/compile_unity.py +0 -46
- unity_devkit-0.1.14/src/unity_devkit/matrix.py +0 -35
- unity_devkit-0.1.14/tests/test_latest_tag_version.py +0 -19
- unity_devkit-0.1.14/tests/test_resolve.py +0 -56
- unity_devkit-0.1.14/uv.lock +0 -366
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/src/unity_devkit/__init__.py +0 -0
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/src/unity_devkit/license.py +0 -0
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/src/unity_devkit/license_restore.py +0 -0
- {unity_devkit-0.1.14 → unity_devkit-0.1.16}/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.
|
|
3
|
+
Version: 0.1.16
|
|
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.
|
|
3
|
+
version = "0.1.16"
|
|
4
4
|
description = "Unity build, license, and CI tooling"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
dependencies = [
|
|
@@ -11,18 +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
|
-
|
|
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"
|
|
21
21
|
install = "unity_devkit.install:app"
|
|
22
22
|
lock-unity = "unity_devkit.lock_unity:app"
|
|
23
23
|
test-unity = "unity_devkit.test_unity:app"
|
|
24
24
|
unity-license-tag = "unity_devkit.license_restore:tag_app"
|
|
25
|
-
unity-
|
|
25
|
+
unity-dispatch = "unity_devkit.dispatch_workflow_generator:app"
|
|
26
26
|
|
|
27
27
|
[build-system]
|
|
28
28
|
requires = ["hatchling"]
|
|
@@ -31,17 +31,3 @@ build-backend = "hatchling.build"
|
|
|
31
31
|
[tool.hatch.build.targets.wheel]
|
|
32
32
|
packages = ["src/unity_devkit"]
|
|
33
33
|
include = ["src/unity_devkit/py.typed"]
|
|
34
|
-
|
|
35
|
-
[tool.uv.sources]
|
|
36
|
-
|
|
37
|
-
[tool.basedpyright]
|
|
38
|
-
venvPath = "."
|
|
39
|
-
venv = ".venv"
|
|
40
|
-
typeCheckingMode = "strict"
|
|
41
|
-
include = ["src", "tests"]
|
|
42
|
-
|
|
43
|
-
[tool.pytest.ini_options]
|
|
44
|
-
testpaths = ["tests"]
|
|
45
|
-
|
|
46
|
-
[tool.python-devkit.preflight]
|
|
47
|
-
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,13 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
3
|
import shutil
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from typing import Annotated
|
|
8
6
|
|
|
9
7
|
import typer
|
|
10
|
-
from bashrun.bash import bash_output
|
|
11
8
|
from pydantic_settings import BaseSettings
|
|
12
9
|
|
|
13
10
|
from ci_devkit.cache import restore, save
|
|
@@ -15,7 +12,7 @@ from ci_devkit.ci_step import ci_step
|
|
|
15
12
|
from .license_restore import restore_license
|
|
16
13
|
from ci_devkit.setup import configure_git, install_dotnet
|
|
17
14
|
from ci_devkit.setup_oras import install_oras
|
|
18
|
-
from .
|
|
15
|
+
from .player_build import build_player, parse_environment_fields, resolve_unity_project
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
class Settings(BaseSettings):
|
|
@@ -26,7 +23,7 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
26
23
|
|
|
27
24
|
|
|
28
25
|
@app.command()
|
|
29
|
-
def
|
|
26
|
+
def ci_build_unity(
|
|
30
27
|
project: Annotated[str, typer.Option(help="Project name")],
|
|
31
28
|
project_path: Annotated[Path, typer.Option(help="Path to Unity project")],
|
|
32
29
|
platform: Annotated[str, typer.Option(help="Target platform")],
|
|
@@ -34,20 +31,19 @@ def main(
|
|
|
34
31
|
registry: Annotated[str, typer.Option(help="OCI registry path")],
|
|
35
32
|
run_number: Annotated[int, typer.Option(help="CI run number")] = 0,
|
|
36
33
|
branch: Annotated[str, typer.Option(help="Git branch name")] = "dev",
|
|
37
|
-
|
|
38
|
-
str, typer.Option(help="
|
|
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")
|
|
39
43
|
] = "",
|
|
40
44
|
) -> None:
|
|
41
45
|
settings = Settings.model_validate({})
|
|
42
|
-
|
|
43
|
-
for line in build_env.splitlines():
|
|
44
|
-
entry = line.strip()
|
|
45
|
-
if not entry:
|
|
46
|
-
continue
|
|
47
|
-
key, separator, value = entry.partition("=")
|
|
48
|
-
if not separator:
|
|
49
|
-
raise SystemExit(f"Invalid --build-env entry (expected KEY=VALUE): {entry!r}")
|
|
50
|
-
os.environ[key.strip()] = value.strip()
|
|
46
|
+
fields = parse_environment_fields(environment_fields.splitlines())
|
|
51
47
|
|
|
52
48
|
with ci_step("Setup"):
|
|
53
49
|
configure_git(settings.github_workspace)
|
|
@@ -64,26 +60,22 @@ def main(
|
|
|
64
60
|
restore(registry, "unity-library", tag, Path("."), fallback_tags=fallback_tags)
|
|
65
61
|
|
|
66
62
|
with ci_step("Prepare build"):
|
|
67
|
-
|
|
68
|
-
unity_project_path = project_config.path
|
|
63
|
+
unity_project_path = resolve_unity_project(project).path
|
|
69
64
|
if unity_project_path.resolve() != project_path.resolve():
|
|
70
65
|
raise SystemExit(
|
|
71
66
|
f"--project-path {project_path} does not match catalog entry '{project}' at {unity_project_path}"
|
|
72
67
|
)
|
|
73
68
|
|
|
74
|
-
with ci_step("Prepare project"):
|
|
75
|
-
prepare_unity_project(unity_project_path)
|
|
76
|
-
|
|
77
69
|
with ci_step(f"Build {project} [{platform}]"):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
)
|
|
87
79
|
|
|
88
80
|
with ci_step("Save library cache"):
|
|
89
81
|
# PackageCache (~1.6 GiB) is redundant with the shared UPM cache at ~/.cache/Unity/upm/
|
|
@@ -98,16 +90,9 @@ def main(
|
|
|
98
90
|
if build_directory.is_dir():
|
|
99
91
|
artifact_directory = Path("/tmp/unity-builds")
|
|
100
92
|
artifact_directory.mkdir(parents=True, exist_ok=True)
|
|
101
|
-
if platform == "
|
|
93
|
+
if platform == "Linux":
|
|
102
94
|
shutil.copytree(build_directory, artifact_directory, dirs_exist_ok=True)
|
|
103
95
|
else:
|
|
104
96
|
for file in build_directory.rglob("*"):
|
|
105
97
|
if file.suffix in {".apk", ".exe"}:
|
|
106
98
|
shutil.copy2(file, artifact_directory / file.name)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def latest_tag_version(prefix: str) -> str | None:
|
|
110
|
-
output = bash_output(f'git tag --list "{prefix}*" --sort=-v:refname').strip()
|
|
111
|
-
if not output:
|
|
112
|
-
return None
|
|
113
|
-
return output.splitlines()[0][len(prefix) :]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from .player_build import prepare_unity_project, resolve_unity_project, run_unity_batchmode
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
8
|
+
|
|
9
|
+
COMPILE_ERROR_SIGNATURES = ("error CS",)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def compile_check_unity(
|
|
14
|
+
project: Annotated[str, typer.Option(help="Unity project name (catalog key in unity-devkit.json)")],
|
|
15
|
+
execute_method: Annotated[
|
|
16
|
+
str | None,
|
|
17
|
+
typer.Option(help="Static method to run after load (Class.Method) — one editor session per invocation"),
|
|
18
|
+
] = None,
|
|
19
|
+
build_target: Annotated[
|
|
20
|
+
str | None,
|
|
21
|
+
typer.Option(help="Startup build target (e.g. Android) for sessions that must open on a non-default platform"),
|
|
22
|
+
] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
project_path = resolve_unity_project(project).path
|
|
25
|
+
extra_flags = ""
|
|
26
|
+
if build_target:
|
|
27
|
+
extra_flags += f" -buildTarget {build_target}"
|
|
28
|
+
if execute_method:
|
|
29
|
+
extra_flags += f" -executeMethod {execute_method}"
|
|
30
|
+
print(f"Compile-checking {project}...")
|
|
31
|
+
prepare_unity_project(project_path)
|
|
32
|
+
run_unity_batchmode(project_path, extra_flags, extra_failure_signatures=COMPILE_ERROR_SIGNATURES)
|
|
33
|
+
print(" Compiles clean")
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
from .compile_check_unity import COMPILE_ERROR_SIGNATURES
|
|
9
|
+
from .projects import load_catalog
|
|
10
|
+
from .player_build import prepare_unity_project, run_unity_batchmode
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
13
|
+
|
|
14
|
+
DUMP_EXECUTE_METHOD = "Outernet.PlayerBuild.DumpEnvironment"
|
|
15
|
+
DISPATCH_INPUT_CAP = 25
|
|
16
|
+
NUMERIC_FIELD_TYPES = {
|
|
17
|
+
"Byte",
|
|
18
|
+
"SByte",
|
|
19
|
+
"Int16",
|
|
20
|
+
"UInt16",
|
|
21
|
+
"Int32",
|
|
22
|
+
"UInt32",
|
|
23
|
+
"Int64",
|
|
24
|
+
"UInt64",
|
|
25
|
+
"Single",
|
|
26
|
+
"Double",
|
|
27
|
+
"Decimal",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EnvironmentEnum(BaseModel):
|
|
32
|
+
model_config = ConfigDict(extra="forbid")
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
values: list[str]
|
|
36
|
+
flags: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EnvironmentFieldDump(BaseModel):
|
|
40
|
+
model_config = ConfigDict(extra="forbid")
|
|
41
|
+
|
|
42
|
+
path: str
|
|
43
|
+
field_type: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EnvironmentDump(BaseModel):
|
|
47
|
+
model_config = ConfigDict(extra="forbid")
|
|
48
|
+
|
|
49
|
+
class_name: str
|
|
50
|
+
mode_field: str
|
|
51
|
+
target_path: str
|
|
52
|
+
presets: dict[str, str]
|
|
53
|
+
enums: list[EnvironmentEnum]
|
|
54
|
+
fields: list[EnvironmentFieldDump]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def unity_dispatch(
|
|
59
|
+
project: Annotated[str, typer.Option(help="Unity project name (catalog key in unity-devkit.json)")],
|
|
60
|
+
output: Annotated[Path, typer.Option(help="Workflow file to write (or check with --check)")],
|
|
61
|
+
build_workflow: Annotated[
|
|
62
|
+
str,
|
|
63
|
+
typer.Option(
|
|
64
|
+
help="The build job's uses: value — the consumer's pinned build-unity.yml reference "
|
|
65
|
+
"(owner/repo/.github/workflows/build-unity.yml@sha, or ./.github/workflows/build-unity.yml inside unity-devkit)"
|
|
66
|
+
),
|
|
67
|
+
],
|
|
68
|
+
environment_config_class: Annotated[
|
|
69
|
+
str | None,
|
|
70
|
+
typer.Option(help="Env class file path override (defaults to build-config.json's environment_config)"),
|
|
71
|
+
] = None,
|
|
72
|
+
check: Annotated[
|
|
73
|
+
bool, typer.Option("--check", help="Compare against the existing file instead of writing; fail on drift")
|
|
74
|
+
] = False,
|
|
75
|
+
) -> None:
|
|
76
|
+
projects = load_catalog()
|
|
77
|
+
if project not in projects:
|
|
78
|
+
raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
|
|
79
|
+
|
|
80
|
+
project_path = projects[project].path
|
|
81
|
+
env = {"ENVIRONMENT_CONFIG_CLASS": environment_config_class} if environment_config_class else None
|
|
82
|
+
print(f"Dumping environment of {project}...")
|
|
83
|
+
prepare_unity_project(project_path)
|
|
84
|
+
log_path = run_unity_batchmode(
|
|
85
|
+
project_path,
|
|
86
|
+
f"-executeMethod {DUMP_EXECUTE_METHOD}",
|
|
87
|
+
extra_failure_signatures=COMPILE_ERROR_SIGNATURES,
|
|
88
|
+
env=env,
|
|
89
|
+
)
|
|
90
|
+
dump = extract_environment_dump(log_path)
|
|
91
|
+
workflow = render_dispatch_workflow(dump, project=project, output=output.as_posix(), build_workflow=build_workflow)
|
|
92
|
+
|
|
93
|
+
if check:
|
|
94
|
+
if not output.is_file():
|
|
95
|
+
raise SystemExit(f"Dispatch workflow drift: {output} does not exist — generate it")
|
|
96
|
+
committed = output.read_text(encoding="utf-8")
|
|
97
|
+
if committed != workflow:
|
|
98
|
+
raise SystemExit(
|
|
99
|
+
f"Dispatch workflow drift: {output} differs from the environment class — regenerate with "
|
|
100
|
+
f"'{regen_command(project, output.as_posix(), build_workflow)}'"
|
|
101
|
+
)
|
|
102
|
+
print(f" {output} matches the environment class")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
output.write_text(workflow, encoding="utf-8")
|
|
107
|
+
print(f" wrote {output}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def extract_environment_dump(log_path: Path) -> EnvironmentDump:
|
|
111
|
+
for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
112
|
+
candidate = line.strip()
|
|
113
|
+
if not candidate.startswith("{"):
|
|
114
|
+
continue
|
|
115
|
+
try:
|
|
116
|
+
payload = json.loads(candidate)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
continue
|
|
119
|
+
if isinstance(payload, dict) and "class_name" in payload:
|
|
120
|
+
return EnvironmentDump.model_validate(payload)
|
|
121
|
+
|
|
122
|
+
raise SystemExit(f"No environment dump JSON found in the editor log at {log_path}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def render_dispatch_workflow(dump: EnvironmentDump, *, project: str, output: str, build_workflow: str) -> str:
|
|
126
|
+
enums_by_name = {environment_enum.name: environment_enum for environment_enum in dump.enums}
|
|
127
|
+
input_blocks: list[tuple[str, str, list[str], str | None]] = [
|
|
128
|
+
(
|
|
129
|
+
"environment-preset",
|
|
130
|
+
"Environment preset (Presets key) — empty leaves the workspace untouched",
|
|
131
|
+
["type: choice", "options:", *indent_lines([*(f'- "{preset}"' for preset in ("", *dump.presets))], 2)],
|
|
132
|
+
'default: ""',
|
|
133
|
+
),
|
|
134
|
+
("development", "Development build column", ["type: boolean"], "default: false"),
|
|
135
|
+
]
|
|
136
|
+
for field in dump.fields:
|
|
137
|
+
if field.path == dump.mode_field:
|
|
138
|
+
continue
|
|
139
|
+
input_blocks.append(field_input_block(field, enums_by_name))
|
|
140
|
+
|
|
141
|
+
input_count = len(input_blocks)
|
|
142
|
+
if input_count > DISPATCH_INPUT_CAP:
|
|
143
|
+
raise SystemExit(
|
|
144
|
+
f"Dispatch panel would carry {input_count} inputs — GitHub's workflow_dispatch cap is {DISPATCH_INPUT_CAP}. "
|
|
145
|
+
"Shrink the env class or move configuration out of it."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
transport_lines = [field_transport_line(field) for field in dump.fields if field.path != dump.mode_field]
|
|
149
|
+
header = (
|
|
150
|
+
f"# Generated by '{regen_command(project, output, build_workflow)}' from the environment class "
|
|
151
|
+
f"{dump.class_name} —"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
lines = [
|
|
155
|
+
header,
|
|
156
|
+
"# edit the class, then regenerate this workflow.",
|
|
157
|
+
"name: Unity Dispatch Build",
|
|
158
|
+
"",
|
|
159
|
+
"on:",
|
|
160
|
+
" workflow_dispatch:",
|
|
161
|
+
" inputs:",
|
|
162
|
+
*indent_lines([line for block in input_blocks for line in build_input_block(block)], 6),
|
|
163
|
+
"",
|
|
164
|
+
"jobs:",
|
|
165
|
+
" build:",
|
|
166
|
+
f" uses: {build_workflow}",
|
|
167
|
+
" with:",
|
|
168
|
+
" environment-preset: ${{ inputs['environment-preset'] }}",
|
|
169
|
+
" development: ${{ inputs.development }}",
|
|
170
|
+
]
|
|
171
|
+
if transport_lines:
|
|
172
|
+
lines.append(" environment-fields: |-")
|
|
173
|
+
lines.extend(indent_lines(transport_lines, 8))
|
|
174
|
+
else:
|
|
175
|
+
lines.append(" environment-fields: ''")
|
|
176
|
+
lines.append(" secrets: inherit")
|
|
177
|
+
return "\n".join(lines) + "\n"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def regen_command(project: str, output: str, build_workflow: str) -> str:
|
|
181
|
+
return f"uv run unity-dispatch --project {project} --output {output} --build-workflow {build_workflow}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def field_input_block(
|
|
185
|
+
field: EnvironmentFieldDump, enums_by_name: dict[str, EnvironmentEnum]
|
|
186
|
+
) -> tuple[str, str, list[str], str | None]:
|
|
187
|
+
input_id = field.path.replace(".", "-")
|
|
188
|
+
if field.field_type == "Boolean":
|
|
189
|
+
return (
|
|
190
|
+
input_id,
|
|
191
|
+
f"environment field {field.path} (bool) — checked overrides the preset value",
|
|
192
|
+
["type: boolean"],
|
|
193
|
+
"default: false",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if field.field_type in enums_by_name:
|
|
197
|
+
environment_enum = enums_by_name[field.field_type]
|
|
198
|
+
if environment_enum.flags:
|
|
199
|
+
return (
|
|
200
|
+
input_id,
|
|
201
|
+
(
|
|
202
|
+
f"environment field {field.path} (flags: {', '.join(environment_enum.values)}) — "
|
|
203
|
+
"comma-separated names, empty keeps the preset value"
|
|
204
|
+
),
|
|
205
|
+
["type: string"],
|
|
206
|
+
'default: ""',
|
|
207
|
+
)
|
|
208
|
+
return (
|
|
209
|
+
input_id,
|
|
210
|
+
f"environment field {field.path} ({environment_enum.name}) — empty keeps the preset value",
|
|
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 (
|
|
221
|
+
input_id,
|
|
222
|
+
f"environment field {field.path} (string) — empty keeps the preset value",
|
|
223
|
+
["type: string"],
|
|
224
|
+
'default: ""',
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if field.field_type in NUMERIC_FIELD_TYPES:
|
|
228
|
+
return (
|
|
229
|
+
input_id,
|
|
230
|
+
f"environment field {field.path} ({field.field_type}) — empty keeps the preset value",
|
|
231
|
+
["type: number"],
|
|
232
|
+
None,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
raise SystemExit(
|
|
236
|
+
f"Environment field '{field.path}' has type '{field.field_type}' — no dispatch input type maps to it "
|
|
237
|
+
"(arrays and object references self-exclude from the dump; this is contract drift)"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def field_transport_line(field: EnvironmentFieldDump) -> str:
|
|
242
|
+
input_id = field.path.replace(".", "-")
|
|
243
|
+
guard = "" if field.field_type == "Boolean" else " != ''"
|
|
244
|
+
return (
|
|
245
|
+
"${{ inputs['"
|
|
246
|
+
+ input_id
|
|
247
|
+
+ "']"
|
|
248
|
+
+ guard
|
|
249
|
+
+ " && format('"
|
|
250
|
+
+ field.path
|
|
251
|
+
+ "={0}', inputs['"
|
|
252
|
+
+ input_id
|
|
253
|
+
+ "']) || '' }}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_input_block(input_block: tuple[str, str, list[str], str | None]) -> list[str]:
|
|
258
|
+
input_id, description, type_lines, default_line = input_block
|
|
259
|
+
lines = [f"{input_id}:", f' description: "{description}"', *[f" {line}" for line in type_lines]]
|
|
260
|
+
if default_line is not None:
|
|
261
|
+
lines.append(f" {default_line}")
|
|
262
|
+
return lines
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def indent_lines(lines: list[str], spaces: int) -> list[str]:
|
|
266
|
+
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,
|
|
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 = {"
|
|
15
|
-
ADB_TARGETS = {"
|
|
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 (
|
|
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
|
-
"
|
|
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 =
|
|
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
|
|
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 == "
|
|
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
|
|
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 .
|
|
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
|
|