unity-devkit 0.2.0.dev215__tar.gz → 0.3.0.dev227__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 (28) hide show
  1. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/PKG-INFO +2 -2
  2. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/pyproject.toml +2 -4
  3. unity_devkit-0.3.0.dev227/src/unity_devkit/build_unity.py +149 -0
  4. unity_devkit-0.3.0.dev227/src/unity_devkit/compile_check_unity.py +40 -0
  5. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/install.py +3 -3
  6. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/license.py +1 -10
  7. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/license_restore.py +3 -3
  8. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/matrix.py +4 -8
  9. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/player_build.py +4 -3
  10. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/upm_cache.py +1 -1
  11. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/tests/test_build_player.py +7 -5
  12. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/tests/test_matrix.py +9 -16
  13. unity_devkit-0.2.0.dev215/src/unity_devkit/build_unity.py +0 -43
  14. unity_devkit-0.2.0.dev215/src/unity_devkit/ci_build_unity.py +0 -159
  15. unity_devkit-0.2.0.dev215/src/unity_devkit/compile_check_unity.py +0 -64
  16. unity_devkit-0.2.0.dev215/tests/fixtures/environment-dump.json +0 -63
  17. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/.gitignore +0 -0
  18. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/__init__.py +0 -0
  19. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/identity.py +0 -0
  20. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/lock_unity.py +0 -0
  21. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/projects.py +0 -0
  22. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/py.typed +0 -0
  23. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/src/unity_devkit/test_unity.py +0 -0
  24. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/tests/test_identity.py +0 -0
  25. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/tests/test_prepare.py +0 -0
  26. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/tests/test_projects.py +0 -0
  27. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/tests/test_run_unity_batchmode.py +0 -0
  28. {unity_devkit-0.2.0.dev215 → unity_devkit-0.3.0.dev227}/tests/test_upm_cache.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unity-devkit
3
- Version: 0.2.0.dev215
3
+ Version: 0.3.0.dev227
4
4
  Summary: Unity build, license, and CI tooling
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: bashrun>=0.1.0
7
- Requires-Dist: ci-devkit>=0.1.15
7
+ Requires-Dist: build-artifact-registry>=0.2.0.dev0
8
8
  Requires-Dist: pydantic-settings>=2.9.1
9
9
  Requires-Dist: pydantic>=2.11.7
10
10
  Requires-Dist: typer>=0.17.4
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "unity-devkit"
3
- version = "0.2.0.dev215"
3
+ version = "0.3.0.dev227"
4
4
  description = "Unity build, license, and CI tooling"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
7
7
  "bashrun>=0.1.0",
8
- "ci-devkit>=0.1.15",
8
+ "build-artifact-registry>=0.2.0.dev0",
9
9
  "typer>=0.17.4",
10
10
  "pydantic>=2.11.7",
11
11
  "pydantic-settings>=2.9.1",
@@ -15,7 +15,6 @@ dependencies = [
15
15
  activate-unity-license = "unity_devkit.license:activate_app"
16
16
  build-unity = "unity_devkit.build_unity:app"
17
17
  build-unity-matrix = "unity_devkit.matrix:build_app"
18
- ci-build-unity = "unity_devkit.ci_build_unity:app"
19
18
  compile-check-unity = "unity_devkit.compile_check_unity:app"
20
19
  compile-check-unity-matrix = "unity_devkit.matrix:compile_check_app"
21
20
  install = "unity_devkit.install:app"
@@ -29,4 +28,3 @@ build-backend = "hatchling.build"
29
28
 
30
29
  [tool.hatch.build.targets.wheel]
31
30
  packages = ["src/unity_devkit"]
32
- include = ["src/unity_devkit/py.typed"]
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from bashrun.bash import bash, bash_output
9
+
10
+ from build_artifact_registry.builds import build_repository, push_build
11
+ from build_artifact_registry.cache import restore, save
12
+ from build_artifact_registry.setup_oras import install_oras
13
+ from .identity import builds_registry, cache_key, cache_registry
14
+ from .license_restore import restore_or_activate_license
15
+ from .player_build import build_player, parse_environment_fields, resolve_unity_project
16
+ from .upm_cache import restore_upm_cache, save_upm_cache
17
+
18
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
19
+
20
+
21
+ @app.command()
22
+ def build_unity(
23
+ project: Annotated[str, typer.Option(help="Project name (the 'name' field of its unity-devkit.json)")],
24
+ platform: Annotated[str, typer.Option(help="Target platform (a key of the project's 'platforms' map)")],
25
+ version: Annotated[
26
+ str,
27
+ typer.Option(
28
+ help="Bare version base; with --commit-count renders bundleVersion={version}+{commit-count}; empty builds unversioned"
29
+ ),
30
+ ] = "",
31
+ pr_number: Annotated[
32
+ str, typer.Option(help="Pull-request number; empty on non-PR runs selects the dev cache scope")
33
+ ] = "",
34
+ commit_count: Annotated[
35
+ int,
36
+ typer.Option(help="Commit count rendering AndroidBundleVersionCode and the +{count} bundleVersion suffix"),
37
+ ] = 0,
38
+ license_cache_key: Annotated[
39
+ str,
40
+ typer.Option("--license-cache-key", help="ORAS tag the license cache restores under; empty uses today's tag"),
41
+ ] = "",
42
+ registry: Annotated[
43
+ str,
44
+ typer.Option(
45
+ help="Build-artifact registry root; supplied runs the CI shape (license/UPM/library caches, "
46
+ "build-output push), empty is the local build"
47
+ ),
48
+ ] = "",
49
+ development: Annotated[bool, typer.Option("--development", help="Development build column")] = False,
50
+ environment_preset: Annotated[
51
+ str, typer.Option(help="Environment preset name; empty leaves the workspace untouched")
52
+ ] = "",
53
+ environment_field: Annotated[
54
+ list[str] | None,
55
+ typer.Option("--environment-field", help="Environment field override as path=value (repeatable)"),
56
+ ] = None,
57
+ ) -> None:
58
+ fields = parse_environment_fields(environment_field or [])
59
+ parsed_pr_number = int(pr_number) if pr_number else None
60
+ derived_cache_key = cache_key(project, platform, parsed_pr_number)
61
+ library_cache_registry = cache_registry(registry)
62
+ builds_namespace = builds_registry(registry)
63
+ project_path = resolve_unity_project(project).path
64
+
65
+ if registry:
66
+ install_oras()
67
+ restore_or_activate_license(library_cache_registry, license_cache_key or None)
68
+ restore_upm_cache(project_path, library_cache_registry)
69
+ restore(library_cache_registry, "unity-library", derived_cache_key, Path("."))
70
+
71
+ artifacts = build_player(
72
+ project,
73
+ platform,
74
+ version=version,
75
+ commit_count=commit_count,
76
+ development=development,
77
+ environment_preset=environment_preset,
78
+ environment_fields=fields,
79
+ )
80
+ for artifact in artifacts:
81
+ print(f"Built: {artifact}")
82
+
83
+ if not registry:
84
+ return
85
+
86
+ # PackageCache (~1.6 GiB) is redundant with the shared UPM cache at ~/.cache/Unity/upm/
87
+ package_cache = project_path / "Library" / "PackageCache"
88
+ if package_cache.exists():
89
+ shutil.rmtree(package_cache)
90
+
91
+ relative_project_path = project_path.relative_to(Path.cwd())
92
+ save(library_cache_registry, "unity-library", derived_cache_key, Path("."), [f"{relative_project_path}/Library/"])
93
+ save_upm_cache(project_path, library_cache_registry)
94
+
95
+ build_directory = project_path / "Build"
96
+ if build_directory.is_dir():
97
+ artifact_directory = Path("/tmp/unity-builds")
98
+ shutil.rmtree(artifact_directory, ignore_errors=True)
99
+ artifact_directory.mkdir(parents=True)
100
+ report_directory = Path("/tmp/unity-build-reports")
101
+ shutil.rmtree(report_directory, ignore_errors=True)
102
+ if platform == "Linux":
103
+ shutil.copytree(build_directory, artifact_directory, dirs_exist_ok=True)
104
+ else:
105
+ binaries = [file for file in build_directory.rglob("*") if file.suffix in {".apk", ".exe"}]
106
+ if len(binaries) != 1:
107
+ raise SystemExit(
108
+ f"build for ({project}, {platform}) must produce exactly one binary, "
109
+ f"found {len(binaries)} ({', '.join(file.name for file in binaries)})"
110
+ )
111
+ shutil.copy2(binaries[0], artifact_directory / binaries[0].name)
112
+ for report in build_directory.rglob("BuildReport.json"):
113
+ destination = report_directory / report.relative_to(build_directory)
114
+ destination.parent.mkdir(parents=True, exist_ok=True)
115
+ shutil.copy2(report, destination)
116
+
117
+ artifact_directory = Path("/tmp/unity-builds")
118
+ if not artifact_directory.is_dir() or not any(artifact_directory.iterdir()):
119
+ print("No build artifacts to push")
120
+ return
121
+
122
+ head_sha = bash_output("git rev-parse HEAD").strip()
123
+ tags = (derived_cache_key, f"sha-{head_sha}")
124
+ if platform == "Linux":
125
+ staging = Path("/tmp/unity-builds-push")
126
+ staging.mkdir(parents=True, exist_ok=True)
127
+ tar_path = staging / "build.tar"
128
+ bash(f"tar -cf {tar_path} -C {artifact_directory} .")
129
+ try:
130
+ for build_tag in tags:
131
+ push_build(builds_namespace, project, platform, build_tag, staging, ["build.tar"])
132
+ finally:
133
+ tar_path.unlink(missing_ok=True)
134
+ else:
135
+ paths = sorted(
136
+ str(path.relative_to(artifact_directory)) for path in artifact_directory.rglob("*") if path.is_file()
137
+ )
138
+ for build_tag in tags:
139
+ push_build(builds_namespace, project, platform, build_tag, artifact_directory, paths)
140
+
141
+ report_directory = Path("/tmp/unity-build-reports")
142
+ if report_directory.is_dir():
143
+ report_paths = sorted(
144
+ str(path.relative_to(report_directory)) for path in report_directory.rglob("BuildReport.json")
145
+ )
146
+ for build_tag in tags:
147
+ push_build(builds_namespace, project, f"{platform}-report", build_tag, report_directory, report_paths)
148
+ repository = build_repository(builds_namespace, project, platform)
149
+ print(f"Pushed build artifacts: {repository} ({', '.join(tags)})")
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from build_artifact_registry.setup_oras import install_oras
8
+ from .identity import cache_registry
9
+ from .license_restore import restore_or_activate_license
10
+ from .player_build import prepare_unity_project, resolve_unity_project, run_unity_batchmode
11
+ from .upm_cache import restore_upm_cache, save_upm_cache
12
+
13
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
14
+
15
+ COMPILE_ERROR_SIGNATURES = ("error CS",)
16
+
17
+
18
+ @app.command()
19
+ def compile_check_unity(
20
+ project: Annotated[str, typer.Option(help="Unity project name (the 'name' field of its unity-devkit.json)")],
21
+ license_cache_key: Annotated[
22
+ str,
23
+ typer.Option("--license-cache-key", help="ORAS tag the license cache restores under; empty uses today's tag"),
24
+ ] = "",
25
+ registry: Annotated[str, typer.Option(help="OCI registry root; the verb derives the cache namespace")] = "",
26
+ ) -> None:
27
+ if not registry:
28
+ raise SystemExit("compile-check needs --registry (the OCI registry root)")
29
+ project_path = resolve_unity_project(project).path
30
+ cache_namespace = cache_registry(registry)
31
+
32
+ install_oras()
33
+ restore_or_activate_license(cache_namespace, license_cache_key or None)
34
+ restore_upm_cache(project_path, cache_namespace)
35
+
36
+ print(f"Compile-checking {project}...")
37
+ prepare_unity_project(project_path)
38
+ run_unity_batchmode(project_path, extra_failure_signatures=COMPILE_ERROR_SIGNATURES)
39
+ save_upm_cache(project_path, cache_namespace)
40
+ print(" Compiles clean")
@@ -8,8 +8,8 @@ from typing import Annotated
8
8
  import typer
9
9
  from bashrun.bash import bash, bash_handoff, bash_output
10
10
 
11
- from ci_devkit.builds import build_reference, build_repository, list_build_tags, pull_build
12
- from ci_devkit.setup_oras import install_oras
11
+ from build_artifact_registry.builds import build_reference, build_repository, list_build_tags, pull_artifact
12
+ from build_artifact_registry.setup_oras import install_oras
13
13
 
14
14
  from .identity import builds_registry, cache_key
15
15
  from .player_build import build_player
@@ -133,7 +133,7 @@ def main(
133
133
  print(f"Using cached build: {cache_path}")
134
134
  else:
135
135
  cache_path.mkdir(parents=True, exist_ok=True)
136
- pull_build(
136
+ pull_artifact(
137
137
  registry,
138
138
  project_name,
139
139
  target_name,
@@ -3,18 +3,10 @@ from __future__ import annotations
3
3
  from typing import Annotated
4
4
 
5
5
  import typer
6
- from pydantic_settings import BaseSettings
7
6
 
8
- from ci_devkit.setup import configure_git
9
- from ci_devkit.setup_oras import install_oras
7
+ from build_artifact_registry.setup_oras import install_oras
10
8
  from .license_restore import activate_license, restore_or_activate_license
11
9
 
12
-
13
- class Settings(BaseSettings):
14
- github_workspace: str
15
-
16
-
17
- settings = Settings.model_validate({})
18
10
  activate_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
19
11
 
20
12
 
@@ -25,7 +17,6 @@ def activate_main(
25
17
  ] = False,
26
18
  registry: Annotated[str, typer.Option(help="OCI registry namespace for the license cache")] = "",
27
19
  ) -> None:
28
- configure_git(settings.github_workspace)
29
20
  install_oras()
30
21
  if oras_push:
31
22
  if not registry:
@@ -9,7 +9,7 @@ import typer
9
9
  from bashrun.bash import CalledProcessError, bash
10
10
  from pydantic_settings import BaseSettings
11
11
 
12
- from ci_devkit.cache import restore, save
12
+ from build_artifact_registry.cache import restore, save
13
13
 
14
14
 
15
15
  class Settings(BaseSettings):
@@ -72,7 +72,7 @@ def license_activated() -> bool:
72
72
  return True
73
73
 
74
74
 
75
- # A whole CI run pins one tag (the matrix verb's license output, passed as
76
- # --license) so a run straddling midnight UTC does not save/restore-miss itself.
75
+ # A whole CI run pins one tag (the getter's unity-license-tag output, passed as
76
+ # --license-cache-key) so a run straddling midnight UTC does not save/restore-miss itself.
77
77
  def license_cache_tag() -> str:
78
78
  return f"v-{datetime.now(UTC).strftime('%Y-%m-%d')}"
@@ -5,7 +5,6 @@ from typing import Annotated
5
5
 
6
6
  import typer
7
7
 
8
- from .license_restore import license_cache_tag
9
8
  from .player_build import LICENSE_IMAGE_MODULE, PLATFORM_CONFIGS, UNITYCI_IMAGE_REVISION, read_editor_version
10
9
  from .projects import ProjectEntry, discover_projects
11
10
 
@@ -29,9 +28,8 @@ def build_matrix(
29
28
  for platform in entry.builds:
30
29
  image_module = PLATFORM_CONFIGS[platform]["unityci_image_module"]
31
30
  matrix.append({
32
- "project-name": name,
31
+ "project": name,
33
32
  "platform": platform,
34
- "unityci-image-module": image_module,
35
33
  "editor-image": f"unityci/editor:{version}-{image_module}-{UNITYCI_IMAGE_REVISION}",
36
34
  })
37
35
 
@@ -40,8 +38,7 @@ def build_matrix(
40
38
  "No projects with builds declared — build-unity-matrix needs at least one unity-devkit.json "
41
39
  "with a non-empty 'platforms' map"
42
40
  )
43
- print(f"matrix={json.dumps({'include': matrix})}")
44
- print(f"license={license_cache_tag()}")
41
+ print(json.dumps(matrix))
45
42
 
46
43
 
47
44
  @compile_check_app.command()
@@ -56,12 +53,11 @@ def compile_check_matrix(
56
53
  for name, entry in projects.items():
57
54
  version = read_editor_version(entry.path)
58
55
  matrix.append({
59
- "project-name": name,
56
+ "project": name,
60
57
  "editor-image": f"unityci/editor:{version}-{LICENSE_IMAGE_MODULE}-{UNITYCI_IMAGE_REVISION}",
61
58
  })
62
59
 
63
- print(f"matrix={json.dumps({'include': matrix})}")
64
- print(f"license={license_cache_tag()}")
60
+ print(json.dumps(matrix))
65
61
 
66
62
 
67
63
  def filtered_projects(project: str | None) -> dict[str, ProjectEntry]:
@@ -197,7 +197,7 @@ def build_player(
197
197
  build: str,
198
198
  *,
199
199
  version: str = "",
200
- version_code: int = 0,
200
+ commit_count: int = 0,
201
201
  development: bool = False,
202
202
  environment_preset: str = "",
203
203
  environment_fields: dict[str, str] | None = None,
@@ -218,12 +218,13 @@ def build_player(
218
218
  prepare_unity_project(project_path)
219
219
 
220
220
  if version:
221
+ bundle_version = f"{version}+{commit_count}"
221
222
  settings_path = project_path / "ProjectSettings" / "ProjectSettings.asset"
222
223
  rewritten = settings_path.read_text(encoding="utf-8")
223
- for field_name, value in (("AndroidBundleVersionCode", str(version_code)), ("bundleVersion", version)):
224
+ for field_name, value in (("AndroidBundleVersionCode", str(commit_count)), ("bundleVersion", bundle_version)):
224
225
  rewritten = replace_serialized_field(rewritten, field_name, value)
225
226
  settings_path.write_text(rewritten, encoding="utf-8")
226
- print(f"Stamped bundleVersion {version} (bundleVersionCode={version_code}) into ProjectSettings")
227
+ print(f"Stamped bundleVersion {bundle_version} (AndroidBundleVersionCode={commit_count}) into ProjectSettings")
227
228
 
228
229
  build_directory = project_path / "Build"
229
230
  before = snapshot_artifacts(build_directory)
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  from pathlib import Path
5
5
 
6
- from ci_devkit.cache import restore, save
6
+ from build_artifact_registry.cache import restore, save
7
7
 
8
8
  UPM_CACHE_NAME = "unity-upm"
9
9
  UPM_CACHE_DIRECTORY = Path.home() / ".cache" / "Unity" / "upm"
@@ -46,7 +46,7 @@ def test_build_player_stamps_version_and_returns_artifacts(tmp_path: Path, monke
46
46
  monkeypatch.chdir(tmp_path)
47
47
  monkeypatch.setattr(player_build, "run_unity_batchmode", fake_unity_build_producing)
48
48
 
49
- produced = player_build.build_player("tool", "AndroidMobile", version="0.2.7-dev+42", version_code=42)
49
+ produced = player_build.build_player("tool", "AndroidMobile", version="0.2.7-dev", commit_count=42)
50
50
 
51
51
  rewritten = (project / "ProjectSettings" / "ProjectSettings.asset").read_text()
52
52
  assert " AndroidBundleVersionCode: 42\n" in rewritten
@@ -56,15 +56,17 @@ def test_build_player_stamps_version_and_returns_artifacts(tmp_path: Path, monke
56
56
  assert RECEIVED_SESSIONS[0][0] == "-buildTarget Android -executeMethod Outernet.PlayerBuild.Entry"
57
57
 
58
58
 
59
- def test_build_player_treats_the_version_string_as_opaque(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
59
+ def test_build_player_appends_the_commit_count_to_an_opaque_version_base(
60
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
61
+ ) -> None:
60
62
  project = write_repository(tmp_path, PROJECT_SETTINGS, builds=["AndroidMobile"])
61
63
  monkeypatch.chdir(tmp_path)
62
64
  monkeypatch.setattr(player_build, "run_unity_batchmode", fake_unity_build_producing)
63
65
 
64
- player_build.build_player("tool", "AndroidMobile", version="release-2026-09-27", version_code=7)
66
+ player_build.build_player("tool", "AndroidMobile", version="release-2026-09-27", commit_count=7)
65
67
 
66
68
  rewritten = (project / "ProjectSettings" / "ProjectSettings.asset").read_text()
67
- assert " bundleVersion: release-2026-09-27\n" in rewritten
69
+ assert " bundleVersion: release-2026-09-27+7\n" in rewritten
68
70
 
69
71
 
70
72
  def test_build_player_refuses_to_stamp_a_missing_field(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -75,7 +77,7 @@ def test_build_player_refuses_to_stamp_a_missing_field(tmp_path: Path, monkeypat
75
77
  monkeypatch.setattr(player_build, "run_unity_batchmode", fake_unity_build_producing)
76
78
 
77
79
  with pytest.raises(SystemExit, match="AndroidBundleVersionCode"):
78
- player_build.build_player("tool", "AndroidMobile", version="0.2.7-dev+42", version_code=42)
80
+ player_build.build_player("tool", "AndroidMobile", version="0.2.7-dev", commit_count=42)
79
81
 
80
82
 
81
83
  def test_build_player_fails_when_no_artifact_was_produced(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -24,17 +24,14 @@ def test_matrix_emits_entries_from_catalog(
24
24
 
25
25
  matrix.build_matrix()
26
26
 
27
- lines = capsys.readouterr().out.splitlines()
28
- include = json.loads(lines[0][len("matrix=") :])["include"]
29
- assert include == [
27
+ legs = json.loads(capsys.readouterr().out)
28
+ assert legs == [
30
29
  {
31
- "project-name": "tool",
30
+ "project": "tool",
32
31
  "platform": "Linux",
33
- "unityci-image-module": "linux-il2cpp",
34
32
  "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3",
35
33
  }
36
34
  ]
37
- assert lines[1].startswith("license=v-")
38
35
 
39
36
 
40
37
  def test_matrix_fails_without_builds(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -57,9 +54,8 @@ def test_matrix_project_filter_scopes_to_one_project(
57
54
 
58
55
  matrix.build_matrix(project="other")
59
56
 
60
- lines = capsys.readouterr().out.splitlines()
61
- include = json.loads(lines[0][len("matrix=") :])["include"]
62
- assert [entry["project-name"] for entry in include] == ["other"]
57
+ legs = json.loads(capsys.readouterr().out)
58
+ assert [entry["project"] for entry in legs] == ["other"]
63
59
 
64
60
 
65
61
  def test_matrix_project_filter_fails_on_unknown_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -78,10 +74,8 @@ def test_check_matrix_includes_path_only_projects(
78
74
 
79
75
  matrix.compile_check_matrix()
80
76
 
81
- lines = capsys.readouterr().out.splitlines()
82
- include = json.loads(lines[0][len("matrix=") :])["include"]
83
- assert include == [{"project-name": "tool", "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3"}]
84
- assert lines[1].startswith("license=v-")
77
+ legs = json.loads(capsys.readouterr().out)
78
+ assert legs == [{"project": "tool", "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3"}]
85
79
 
86
80
 
87
81
  def test_check_matrix_project_filter_scopes_to_one_project(
@@ -96,6 +90,5 @@ def test_check_matrix_project_filter_scopes_to_one_project(
96
90
 
97
91
  matrix.compile_check_matrix(project="other")
98
92
 
99
- lines = capsys.readouterr().out.splitlines()
100
- include = json.loads(lines[0][len("matrix=") :])["include"]
101
- assert include == [{"project-name": "other", "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3"}]
93
+ legs = json.loads(capsys.readouterr().out)
94
+ assert legs == [{"project": "other", "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3"}]
@@ -1,43 +0,0 @@
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 (the 'name' field of its 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
- version_code: Annotated[
19
- int, typer.Option(help="Android bundleVersionCode to stamp; local builds default to 0")
20
- ] = 0,
21
- development: Annotated[bool, typer.Option(help="Development build column")] = False,
22
- environment_preset: Annotated[
23
- str,
24
- typer.Option(
25
- help="Environment preset name (the env class's Presets key); unset leaves the workspace untouched"
26
- ),
27
- ] = "",
28
- environment_field: Annotated[
29
- list[str] | None,
30
- typer.Option("--environment-field", help="Environment field override as path=value (repeatable)"),
31
- ] = None,
32
- ) -> None:
33
- fields = parse_environment_fields(environment_field or [])
34
- for artifact in build_player(
35
- project,
36
- build,
37
- version=version,
38
- version_code=version_code,
39
- development=development,
40
- environment_preset=environment_preset,
41
- environment_fields=fields,
42
- ):
43
- print(f"Built: {artifact}")
@@ -1,159 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import shutil
4
- from pathlib import Path
5
- from typing import Annotated
6
-
7
- import typer
8
- from bashrun.bash import bash, bash_output
9
- from pydantic_settings import BaseSettings
10
-
11
- from ci_devkit.builds import build_repository, push_build
12
- from ci_devkit.cache import restore, save
13
- from ci_devkit.ci_step import ci_step
14
- from ci_devkit.setup import configure_git, install_dotnet
15
- from ci_devkit.setup_oras import install_oras
16
- from .identity import builds_registry, cache_key, cache_registry
17
- from .license_restore import restore_or_activate_license
18
- from .player_build import build_player, parse_environment_fields, resolve_unity_project
19
- from .upm_cache import restore_upm_cache, save_upm_cache
20
-
21
-
22
- class Settings(BaseSettings):
23
- github_workspace: str
24
-
25
-
26
- app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
27
-
28
-
29
- @app.command()
30
- def ci_build_unity(
31
- project: Annotated[str, typer.Option(help="Project name")],
32
- platform: Annotated[str, typer.Option(help="Target platform")],
33
- version_code: Annotated[
34
- int, typer.Option(help="Android bundleVersionCode (the per-app commit count from get-app-version)")
35
- ],
36
- registry: Annotated[str, typer.Option(help="OCI registry root; the verb derives the cache and builds namespaces")],
37
- pr_number: Annotated[
38
- str, typer.Option(help="Pull-request number; empty on non-PR runs selects the dev cache scope")
39
- ] = "",
40
- license_pin: Annotated[
41
- str,
42
- typer.Option(
43
- "--license", help="License cache pin for this run (the matrix verb's license output; empty uses today's)"
44
- ),
45
- ] = "",
46
- environment_preset: Annotated[
47
- str, typer.Option(help="Environment preset name; empty leaves the workspace untouched")
48
- ] = "",
49
- development: Annotated[bool, typer.Option("--development", help="Development build column")] = False,
50
- environment_fields: Annotated[
51
- str, typer.Option(help="Newline-separated path=value environment field overrides")
52
- ] = "",
53
- version: Annotated[
54
- str, typer.Option(help="Version string to stamp into ProjectSettings.asset; empty builds unversioned")
55
- ] = "",
56
- ) -> None:
57
- settings = Settings.model_validate({})
58
- fields = parse_environment_fields(environment_fields.splitlines())
59
- parsed_pr_number = int(pr_number) if pr_number else None
60
- derived_cache_key = cache_key(project, platform, parsed_pr_number)
61
- library_cache_registry = cache_registry(registry)
62
- builds_namespace = builds_registry(registry)
63
-
64
- with ci_step("Setup"):
65
- configure_git(settings.github_workspace)
66
- install_dotnet("8.0")
67
- install_oras()
68
- restore_or_activate_license(library_cache_registry, license_pin or None)
69
- project_path = resolve_unity_project(project).path
70
- restore_upm_cache(project_path, library_cache_registry)
71
-
72
- with ci_step("Restore library cache"):
73
- restore(library_cache_registry, "unity-library", derived_cache_key, Path("."))
74
-
75
- with ci_step(f"Build {project} [{platform}]"):
76
- build_player(
77
- project,
78
- platform,
79
- version=version,
80
- version_code=version_code,
81
- development=development,
82
- environment_preset=environment_preset,
83
- environment_fields=fields,
84
- )
85
-
86
- with ci_step("Save library cache"):
87
- # PackageCache (~1.6 GiB) is redundant with the shared UPM cache at ~/.cache/Unity/upm/
88
- package_cache = project_path / "Library" / "PackageCache"
89
- if package_cache.exists():
90
- shutil.rmtree(package_cache)
91
-
92
- relative_project_path = project_path.relative_to(Path.cwd())
93
- save(
94
- library_cache_registry, "unity-library", derived_cache_key, Path("."), [f"{relative_project_path}/Library/"]
95
- )
96
-
97
- with ci_step("Save UPM cache"):
98
- save_upm_cache(project_path, library_cache_registry)
99
-
100
- with ci_step("Collect build artifacts"):
101
- build_directory = project_path / "Build"
102
- if build_directory.is_dir():
103
- artifact_directory = Path("/tmp/unity-builds")
104
- shutil.rmtree(artifact_directory, ignore_errors=True)
105
- artifact_directory.mkdir(parents=True)
106
- report_directory = Path("/tmp/unity-build-reports")
107
- shutil.rmtree(report_directory, ignore_errors=True)
108
- if platform == "Linux":
109
- shutil.copytree(build_directory, artifact_directory, dirs_exist_ok=True)
110
- else:
111
- binaries = [file for file in build_directory.rglob("*") if file.suffix in {".apk", ".exe"}]
112
- if len(binaries) != 1:
113
- raise SystemExit(
114
- f"build for ({project}, {platform}) must produce exactly one binary, "
115
- f"found {len(binaries)} ({', '.join(file.name for file in binaries)})"
116
- )
117
- shutil.copy2(binaries[0], artifact_directory / binaries[0].name)
118
- for report in build_directory.rglob("BuildReport.json"):
119
- destination = report_directory / report.relative_to(build_directory)
120
- destination.parent.mkdir(parents=True, exist_ok=True)
121
- shutil.copy2(report, destination)
122
-
123
- with ci_step("Push build artifacts"):
124
- artifact_directory = Path("/tmp/unity-builds")
125
- if not artifact_directory.is_dir() or not any(artifact_directory.iterdir()):
126
- print("No build artifacts to push")
127
- else:
128
- head_sha = bash_output("git rev-parse HEAD").strip()
129
- tags = (derived_cache_key, f"sha-{head_sha}")
130
- if platform == "Linux":
131
- staging = Path("/tmp/unity-builds-push")
132
- staging.mkdir(parents=True, exist_ok=True)
133
- tar_path = staging / "build.tar"
134
- bash(f"tar -cf {tar_path} -C {artifact_directory} .")
135
- try:
136
- for build_tag in tags:
137
- push_build(builds_namespace, project, platform, build_tag, staging, ["build.tar"])
138
- finally:
139
- tar_path.unlink(missing_ok=True)
140
- else:
141
- paths = sorted(
142
- str(path.relative_to(artifact_directory))
143
- for path in artifact_directory.rglob("*")
144
- if path.is_file()
145
- )
146
- for build_tag in tags:
147
- push_build(builds_namespace, project, platform, build_tag, artifact_directory, paths)
148
-
149
- report_directory = Path("/tmp/unity-build-reports")
150
- if report_directory.is_dir():
151
- report_paths = sorted(
152
- str(path.relative_to(report_directory)) for path in report_directory.rglob("BuildReport.json")
153
- )
154
- for build_tag in tags:
155
- push_build(
156
- builds_namespace, project, f"{platform}-report", build_tag, report_directory, report_paths
157
- )
158
- repository = build_repository(builds_namespace, project, platform)
159
- print(f"Pushed build artifacts: {repository} ({', '.join(tags)})")
@@ -1,64 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Annotated
4
-
5
- import typer
6
- from pydantic_settings import BaseSettings
7
-
8
- from ci_devkit.ci_step import ci_step
9
- from ci_devkit.setup import configure_git
10
- from ci_devkit.setup_oras import install_oras
11
- from .identity import cache_registry
12
- from .license_restore import restore_or_activate_license
13
- from .player_build import prepare_unity_project, resolve_unity_project, run_unity_batchmode
14
- from .upm_cache import restore_upm_cache, save_upm_cache
15
-
16
-
17
- class Settings(BaseSettings):
18
- github_workspace: str
19
-
20
-
21
- app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
22
-
23
- COMPILE_ERROR_SIGNATURES = ("error CS",)
24
-
25
-
26
- @app.command()
27
- def compile_check_unity(
28
- project: Annotated[str, typer.Option(help="Unity project name (the 'name' field of its unity-devkit.json)")],
29
- registry: Annotated[str, typer.Option(help="OCI registry root; the verb derives the cache namespace")],
30
- license_pin: Annotated[
31
- str,
32
- typer.Option(
33
- "--license", help="License cache pin for this run (the matrix verb's license output; empty uses today's)"
34
- ),
35
- ] = "",
36
- execute_method: Annotated[
37
- str | None,
38
- typer.Option(help="Static method to run after load (Class.Method) — one editor session per invocation"),
39
- ] = None,
40
- build_target: Annotated[
41
- str | None,
42
- typer.Option(help="Startup build target (e.g. Android) for sessions that must open on a non-default platform"),
43
- ] = None,
44
- ) -> None:
45
- settings = Settings.model_validate({})
46
- project_path = resolve_unity_project(project).path
47
- cache_namespace = cache_registry(registry)
48
-
49
- with ci_step("Setup"):
50
- configure_git(settings.github_workspace)
51
- install_oras()
52
- restore_or_activate_license(cache_namespace, license_pin or None)
53
- restore_upm_cache(project_path, cache_namespace)
54
-
55
- extra_flags = ""
56
- if build_target:
57
- extra_flags += f" -buildTarget {build_target}"
58
- if execute_method:
59
- extra_flags += f" -executeMethod {execute_method}"
60
- print(f"Compile-checking {project}...")
61
- prepare_unity_project(project_path)
62
- run_unity_batchmode(project_path, extra_flags, extra_failure_signatures=COMPILE_ERROR_SIGNATURES)
63
- save_upm_cache(project_path, cache_namespace)
64
- print(" Compiles clean")
@@ -1,63 +0,0 @@
1
- {
2
- "class_name": "Outernet.FixtureEnv",
3
- "mode_field": "configMode",
4
- "target_path": "Assets/_LocalWorkspace/Resources/FixtureEnv.asset",
5
- "presets": {
6
- "Airgapped": "Assets/Editor/FixtureAirgappedEnv.asset"
7
- },
8
- "enums": [
9
- {
10
- "name": "FixtureEnvMode",
11
- "values": [
12
- "Airgapped",
13
- "Supabase",
14
- "Override"
15
- ],
16
- "flags": false
17
- },
18
- {
19
- "name": "FixtureCapabilities",
20
- "values": [
21
- "None",
22
- "Capture",
23
- "Stream",
24
- "Analyze"
25
- ],
26
- "flags": true
27
- }
28
- ],
29
- "fields": [
30
- {
31
- "path": "configMode",
32
- "field_type": "FixtureEnvMode"
33
- },
34
- {
35
- "path": "username",
36
- "field_type": "String"
37
- },
38
- {
39
- "path": "password",
40
- "field_type": "String"
41
- },
42
- {
43
- "path": "verboseLogging",
44
- "field_type": "Boolean"
45
- },
46
- {
47
- "path": "pollIntervalSeconds",
48
- "field_type": "Single"
49
- },
50
- {
51
- "path": "capabilities",
52
- "field_type": "FixtureCapabilities"
53
- },
54
- {
55
- "path": "localConfig.apiUrl",
56
- "field_type": "String"
57
- },
58
- {
59
- "path": "localConfig.portNumber",
60
- "field_type": "Int32"
61
- }
62
- ]
63
- }