unity-devkit 0.1.20__tar.gz → 0.1.21__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.20 → unity_devkit-0.1.21}/PKG-INFO +1 -1
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/pyproject.toml +3 -4
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/ci_build_unity.py +21 -12
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/compile_check_unity.py +22 -0
- unity_devkit-0.1.21/src/unity_devkit/license.py +32 -0
- unity_devkit-0.1.21/src/unity_devkit/license_restore.py +84 -0
- unity_devkit-0.1.21/src/unity_devkit/matrix.py +76 -0
- unity_devkit-0.1.21/src/unity_devkit/upm_cache.py +39 -0
- unity_devkit-0.1.21/tests/test_matrix.py +118 -0
- unity_devkit-0.1.21/tests/test_upm_cache.py +37 -0
- unity_devkit-0.1.20/src/unity_devkit/dispatch_workflow_generator.py +0 -268
- unity_devkit-0.1.20/src/unity_devkit/license.py +0 -47
- unity_devkit-0.1.20/src/unity_devkit/license_restore.py +0 -38
- unity_devkit-0.1.20/src/unity_devkit/matrix.py +0 -52
- unity_devkit-0.1.20/tests/test_dispatch_workflow_generator.py +0 -225
- unity_devkit-0.1.20/tests/test_matrix.py +0 -61
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/.gitignore +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/__init__.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/build_unity.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/install.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/lock_unity.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/player_build.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/projects.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/py.typed +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/src/unity_devkit/test_unity.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/tests/fixtures/environment-dump.json +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/tests/test_build_player.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/tests/test_prepare.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/tests/test_projects.py +0 -0
- {unity_devkit-0.1.20 → unity_devkit-0.1.21}/tests/test_run_unity_batchmode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unity-devkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.21"
|
|
4
4
|
description = "Unity build, license, and CI tooling"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
dependencies = [
|
|
@@ -14,15 +14,14 @@ dependencies = [
|
|
|
14
14
|
[project.scripts]
|
|
15
15
|
activate-unity-license = "unity_devkit.license:activate_app"
|
|
16
16
|
build-unity = "unity_devkit.build_unity:app"
|
|
17
|
-
build-unity-matrix = "unity_devkit.matrix:
|
|
17
|
+
build-unity-matrix = "unity_devkit.matrix:build_app"
|
|
18
18
|
ci-build-unity = "unity_devkit.ci_build_unity:app"
|
|
19
19
|
compile-check-unity = "unity_devkit.compile_check_unity:app"
|
|
20
|
-
compile-check-unity-matrix = "unity_devkit.matrix:
|
|
20
|
+
compile-check-unity-matrix = "unity_devkit.matrix:compile_check_app"
|
|
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-dispatch = "unity_devkit.dispatch_workflow_generator:app"
|
|
26
25
|
|
|
27
26
|
[build-system]
|
|
28
27
|
requires = ["hatchling"]
|
|
@@ -10,10 +10,11 @@ from pydantic_settings import BaseSettings
|
|
|
10
10
|
|
|
11
11
|
from ci_devkit.cache import restore, save
|
|
12
12
|
from ci_devkit.ci_step import ci_step
|
|
13
|
-
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
|
|
15
|
+
from .license_restore import restore_or_activate_license
|
|
16
16
|
from .player_build import build_player, parse_environment_fields, resolve_unity_project
|
|
17
|
+
from .upm_cache import restore_upm_cache, save_upm_cache
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class Settings(BaseSettings):
|
|
@@ -26,7 +27,6 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
26
27
|
@app.command()
|
|
27
28
|
def ci_build_unity(
|
|
28
29
|
project: Annotated[str, typer.Option(help="Project name")],
|
|
29
|
-
project_path: Annotated[Path, typer.Option(help="Path to Unity project")],
|
|
30
30
|
platform: Annotated[str, typer.Option(help="Target platform")],
|
|
31
31
|
cache_key: Annotated[str, typer.Option(help="Cache key prefix")],
|
|
32
32
|
registry: Annotated[str, typer.Option(help="OCI registry path for the library cache")],
|
|
@@ -51,7 +51,9 @@ def ci_build_unity(
|
|
|
51
51
|
configure_git(settings.github_workspace)
|
|
52
52
|
install_dotnet("8.0")
|
|
53
53
|
install_oras()
|
|
54
|
-
|
|
54
|
+
restore_or_activate_license()
|
|
55
|
+
project_path = resolve_unity_project(project).path
|
|
56
|
+
restore_upm_cache(project_path)
|
|
55
57
|
|
|
56
58
|
branch_slug = branch.replace("/", "-")
|
|
57
59
|
tag = f"{cache_key}-{platform}-{branch_slug}"
|
|
@@ -61,13 +63,6 @@ def ci_build_unity(
|
|
|
61
63
|
with ci_step("Restore library cache"):
|
|
62
64
|
restore(registry, "unity-library", tag, Path("."), fallback_tags=fallback_tags)
|
|
63
65
|
|
|
64
|
-
with ci_step("Prepare build"):
|
|
65
|
-
unity_project_path = resolve_unity_project(project).path
|
|
66
|
-
if unity_project_path.resolve() != project_path.resolve():
|
|
67
|
-
raise SystemExit(
|
|
68
|
-
f"--project-path {project_path} does not match discovered project '{project}' at {unity_project_path}"
|
|
69
|
-
)
|
|
70
|
-
|
|
71
66
|
with ci_step(f"Build {project} [{platform}]"):
|
|
72
67
|
build_player(
|
|
73
68
|
project,
|
|
@@ -85,7 +80,11 @@ def ci_build_unity(
|
|
|
85
80
|
if package_cache.exists():
|
|
86
81
|
shutil.rmtree(package_cache)
|
|
87
82
|
|
|
88
|
-
|
|
83
|
+
relative_project_path = project_path.relative_to(Path.cwd())
|
|
84
|
+
save(registry, "unity-library", tag, Path("."), [f"{relative_project_path}/Library/"])
|
|
85
|
+
|
|
86
|
+
with ci_step("Save UPM cache"):
|
|
87
|
+
save_upm_cache(project_path)
|
|
89
88
|
|
|
90
89
|
with ci_step("Collect build artifacts"):
|
|
91
90
|
build_directory = project_path / "Build"
|
|
@@ -98,6 +97,10 @@ def ci_build_unity(
|
|
|
98
97
|
for file in build_directory.rglob("*"):
|
|
99
98
|
if file.suffix in {".apk", ".exe"}:
|
|
100
99
|
shutil.copy2(file, artifact_directory / file.name)
|
|
100
|
+
for report in build_directory.rglob("BuildReport.json"):
|
|
101
|
+
destination = artifact_directory / report.relative_to(build_directory)
|
|
102
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
shutil.copy2(report, destination)
|
|
101
104
|
|
|
102
105
|
with ci_step("Push build artifacts"):
|
|
103
106
|
artifact_directory = Path("/tmp/unity-builds")
|
|
@@ -117,7 +120,13 @@ def ci_build_unity(
|
|
|
117
120
|
finally:
|
|
118
121
|
tar_path.unlink(missing_ok=True)
|
|
119
122
|
else:
|
|
120
|
-
joined = " ".join(
|
|
123
|
+
joined = " ".join(
|
|
124
|
+
sorted(
|
|
125
|
+
str(path.relative_to(artifact_directory))
|
|
126
|
+
for path in artifact_directory.rglob("*")
|
|
127
|
+
if path.is_file()
|
|
128
|
+
)
|
|
129
|
+
)
|
|
121
130
|
for build_tag in tags:
|
|
122
131
|
bash(f"oras push {reference_base}:{build_tag} {joined}", cwd=artifact_directory)
|
|
123
132
|
print(f"Pushed build artifacts: {reference_base} ({', '.join(tags)})")
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from typing import Annotated
|
|
2
4
|
|
|
3
5
|
import typer
|
|
6
|
+
from pydantic_settings import BaseSettings
|
|
4
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 .license_restore import restore_or_activate_license
|
|
5
12
|
from .player_build import prepare_unity_project, resolve_unity_project, run_unity_batchmode
|
|
13
|
+
from .upm_cache import restore_upm_cache, save_upm_cache
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Settings(BaseSettings):
|
|
17
|
+
github_workspace: str
|
|
18
|
+
|
|
6
19
|
|
|
7
20
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
8
21
|
|
|
@@ -21,7 +34,15 @@ def compile_check_unity(
|
|
|
21
34
|
typer.Option(help="Startup build target (e.g. Android) for sessions that must open on a non-default platform"),
|
|
22
35
|
] = None,
|
|
23
36
|
) -> None:
|
|
37
|
+
settings = Settings.model_validate({})
|
|
24
38
|
project_path = resolve_unity_project(project).path
|
|
39
|
+
|
|
40
|
+
with ci_step("Setup"):
|
|
41
|
+
configure_git(settings.github_workspace)
|
|
42
|
+
install_oras()
|
|
43
|
+
restore_or_activate_license()
|
|
44
|
+
restore_upm_cache(project_path)
|
|
45
|
+
|
|
25
46
|
extra_flags = ""
|
|
26
47
|
if build_target:
|
|
27
48
|
extra_flags += f" -buildTarget {build_target}"
|
|
@@ -30,4 +51,5 @@ def compile_check_unity(
|
|
|
30
51
|
print(f"Compile-checking {project}...")
|
|
31
52
|
prepare_unity_project(project_path)
|
|
32
53
|
run_unity_batchmode(project_path, extra_flags, extra_failure_signatures=COMPILE_ERROR_SIGNATURES)
|
|
54
|
+
save_upm_cache(project_path)
|
|
33
55
|
print(" Compiles clean")
|
|
@@ -0,0 +1,32 @@
|
|
|
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.setup import configure_git
|
|
9
|
+
from ci_devkit.setup_oras import install_oras
|
|
10
|
+
from .license_restore import activate_license, restore_or_activate_license
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Settings(BaseSettings):
|
|
14
|
+
github_workspace: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
settings = Settings.model_validate({})
|
|
18
|
+
activate_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@activate_app.command()
|
|
22
|
+
def activate_main(
|
|
23
|
+
oras_push: Annotated[
|
|
24
|
+
bool, typer.Option(help="Restore from the ORAS cache on miss and push the activated ULF back")
|
|
25
|
+
] = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
configure_git(settings.github_workspace)
|
|
28
|
+
install_oras()
|
|
29
|
+
if oras_push:
|
|
30
|
+
restore_or_activate_license()
|
|
31
|
+
else:
|
|
32
|
+
activate_license()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from bashrun.bash import CalledProcessError, bash
|
|
11
|
+
from pydantic_settings import BaseSettings
|
|
12
|
+
|
|
13
|
+
from ci_devkit.cache import restore, save
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Settings(BaseSettings):
|
|
17
|
+
cache_registry: str
|
|
18
|
+
unity_email: str
|
|
19
|
+
unity_password: str
|
|
20
|
+
unity_serial: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
tag_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
24
|
+
|
|
25
|
+
LICENSE_CACHE_NAME = "unity-license"
|
|
26
|
+
ACTIVATION_ATTEMPTS = 3
|
|
27
|
+
RETRY_JITTER_SECONDS = (30.0, 120.0)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@tag_app.command()
|
|
31
|
+
def tag_main() -> None:
|
|
32
|
+
print(license_cache_tag())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def restore_or_activate_license() -> None:
|
|
36
|
+
settings = Settings.model_validate({})
|
|
37
|
+
license_directory = Path.home() / ".local" / "share" / "unity3d" / "Unity"
|
|
38
|
+
license_directory.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
tag = license_cache_tag()
|
|
40
|
+
|
|
41
|
+
for attempt in range(1, ACTIVATION_ATTEMPTS + 1):
|
|
42
|
+
if restore(settings.cache_registry, LICENSE_CACHE_NAME, tag, license_directory):
|
|
43
|
+
return
|
|
44
|
+
if license_activated():
|
|
45
|
+
save(settings.cache_registry, LICENSE_CACHE_NAME, tag, license_directory, ["Unity_lic.ulf"])
|
|
46
|
+
return
|
|
47
|
+
if attempt == ACTIVATION_ATTEMPTS:
|
|
48
|
+
break
|
|
49
|
+
wait = random.SystemRandom().uniform(*RETRY_JITTER_SECONDS)
|
|
50
|
+
print(
|
|
51
|
+
f"License activation rejected (attempt {attempt}/{ACTIVATION_ATTEMPTS}) — "
|
|
52
|
+
f"waiting {wait:.0f}s, then re-restoring and retrying"
|
|
53
|
+
)
|
|
54
|
+
time.sleep(wait)
|
|
55
|
+
|
|
56
|
+
raise SystemExit(
|
|
57
|
+
f"Unity license activation failed after {ACTIVATION_ATTEMPTS} attempts — "
|
|
58
|
+
"check UNITY_EMAIL/UNITY_PASSWORD/UNITY_SERIAL and the license seat"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def activate_license() -> None:
|
|
63
|
+
settings = Settings.model_validate({})
|
|
64
|
+
bash(
|
|
65
|
+
f'unity-editor -batchmode -nographics -quit -serial "{settings.unity_serial}"'
|
|
66
|
+
f' -username "{settings.unity_email}" -password "{settings.unity_password}" -logFile /dev/stdout'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def license_activated() -> bool:
|
|
71
|
+
try:
|
|
72
|
+
activate_license()
|
|
73
|
+
except CalledProcessError:
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# LICENSE_CACHE_TAG env pins the tag for a whole CI run; without it, each call
|
|
79
|
+
# re-reads "now UTC" and a run straddling midnight save/restore-misses itself.
|
|
80
|
+
def license_cache_tag() -> str:
|
|
81
|
+
override = os.environ.get("LICENSE_CACHE_TAG")
|
|
82
|
+
if override:
|
|
83
|
+
return override
|
|
84
|
+
return f"v-{datetime.now(UTC).strftime('%Y-%m-%d')}"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from .license_restore import license_cache_tag
|
|
10
|
+
from .player_build import LICENSE_IMAGE_MODULE, PLATFORM_CONFIGS, UNITYCI_IMAGE_REVISION, read_editor_version
|
|
11
|
+
from .projects import ProjectEntry, discover_projects
|
|
12
|
+
|
|
13
|
+
build_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
14
|
+
compile_check_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@build_app.command()
|
|
18
|
+
def build_matrix(
|
|
19
|
+
project: Annotated[
|
|
20
|
+
str | None, typer.Option(help="Restrict the matrix to one project (dispatch scoping in multi-app repos)")
|
|
21
|
+
] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
projects = filtered_projects(project)
|
|
24
|
+
matrix: list[dict[str, str]] = []
|
|
25
|
+
|
|
26
|
+
for name, entry in projects.items():
|
|
27
|
+
if not entry.builds:
|
|
28
|
+
continue
|
|
29
|
+
version = read_editor_version(entry.path)
|
|
30
|
+
for platform in entry.builds:
|
|
31
|
+
image_module = PLATFORM_CONFIGS[platform]["unityci_image_module"]
|
|
32
|
+
matrix.append({
|
|
33
|
+
"project": str(entry.path.relative_to(Path.cwd())),
|
|
34
|
+
"project-name": name,
|
|
35
|
+
"cache-key": name.lower(),
|
|
36
|
+
"platform": platform,
|
|
37
|
+
"unityci-image-module": image_module,
|
|
38
|
+
"editor-image": f"unityci/editor:{version}-{image_module}-{UNITYCI_IMAGE_REVISION}",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if not matrix:
|
|
42
|
+
raise SystemExit(
|
|
43
|
+
"No projects with builds declared — build-unity-matrix needs at least one unity-devkit.json "
|
|
44
|
+
"with a non-empty 'platforms' map"
|
|
45
|
+
)
|
|
46
|
+
print(f"matrix={json.dumps({'include': matrix})}")
|
|
47
|
+
print(f"license-tag={license_cache_tag()}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@compile_check_app.command()
|
|
51
|
+
def compile_check_matrix(
|
|
52
|
+
project: Annotated[
|
|
53
|
+
str | None, typer.Option(help="Restrict the matrix to one project (dispatch scoping in multi-app repos)")
|
|
54
|
+
] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
projects = filtered_projects(project)
|
|
57
|
+
matrix: list[dict[str, str]] = []
|
|
58
|
+
|
|
59
|
+
for name, entry in projects.items():
|
|
60
|
+
version = read_editor_version(entry.path)
|
|
61
|
+
matrix.append({
|
|
62
|
+
"project-name": name,
|
|
63
|
+
"editor-image": f"unityci/editor:{version}-{LICENSE_IMAGE_MODULE}-{UNITYCI_IMAGE_REVISION}",
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
print(f"matrix={json.dumps({'include': matrix})}")
|
|
67
|
+
print(f"license-tag={license_cache_tag()}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def filtered_projects(project: str | None) -> dict[str, ProjectEntry]:
|
|
71
|
+
projects = discover_projects()
|
|
72
|
+
if project is None:
|
|
73
|
+
return projects
|
|
74
|
+
if project not in projects:
|
|
75
|
+
raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
|
|
76
|
+
return {project: projects[project]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pydantic_settings import BaseSettings
|
|
7
|
+
|
|
8
|
+
from ci_devkit.cache import restore, save
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Settings(BaseSettings):
|
|
12
|
+
cache_registry: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
UPM_CACHE_NAME = "unity-upm"
|
|
16
|
+
UPM_CACHE_DIRECTORY = Path.home() / ".cache" / "Unity" / "upm"
|
|
17
|
+
UPM_MANIFESTS = ("manifest.json", "packages-lock.json")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def restore_upm_cache(project_path: Path) -> None:
|
|
21
|
+
settings = Settings.model_validate({})
|
|
22
|
+
UPM_CACHE_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
restore(settings.cache_registry, UPM_CACHE_NAME, upm_cache_tag(project_path), UPM_CACHE_DIRECTORY)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_upm_cache(project_path: Path) -> None:
|
|
27
|
+
settings = Settings.model_validate({})
|
|
28
|
+
UPM_CACHE_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
save(settings.cache_registry, UPM_CACHE_NAME, upm_cache_tag(project_path), UPM_CACHE_DIRECTORY, ["."])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# A never-resolved project has no packages-lock.json yet; hashing it as empty keeps the
|
|
33
|
+
# tag computable, and the lock written by that run produces the tag the next run restores.
|
|
34
|
+
def upm_cache_tag(project_path: Path) -> str:
|
|
35
|
+
digest = hashlib.sha256()
|
|
36
|
+
for name in UPM_MANIFESTS:
|
|
37
|
+
manifest = project_path / "Packages" / name
|
|
38
|
+
digest.update(manifest.read_bytes() if manifest.exists() else b"")
|
|
39
|
+
return digest.hexdigest()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from unity_devkit import matrix
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def write_repository(tmp_path: Path, builds: list[str] | None = None) -> None:
|
|
10
|
+
project = tmp_path / "apps" / "Tool"
|
|
11
|
+
(project / "ProjectSettings").mkdir(parents=True)
|
|
12
|
+
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
13
|
+
config: dict[str, object] = {"name": "tool"}
|
|
14
|
+
if builds is not None:
|
|
15
|
+
config["platforms"] = {platform: {} for platform in builds}
|
|
16
|
+
(project / "unity-devkit.json").write_text(json.dumps(config))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_matrix_emits_entries_from_catalog(
|
|
20
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
21
|
+
) -> None:
|
|
22
|
+
write_repository(tmp_path, builds=["Linux"])
|
|
23
|
+
monkeypatch.chdir(tmp_path)
|
|
24
|
+
monkeypatch.delenv("LICENSE_CACHE_TAG", raising=False)
|
|
25
|
+
|
|
26
|
+
matrix.build_matrix()
|
|
27
|
+
|
|
28
|
+
lines = capsys.readouterr().out.splitlines()
|
|
29
|
+
include = json.loads(lines[0][len("matrix=") :])["include"]
|
|
30
|
+
assert include == [
|
|
31
|
+
{
|
|
32
|
+
"project": str(Path("apps") / "Tool"),
|
|
33
|
+
"project-name": "tool",
|
|
34
|
+
"cache-key": "tool",
|
|
35
|
+
"platform": "Linux",
|
|
36
|
+
"unityci-image-module": "linux-il2cpp",
|
|
37
|
+
"editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3",
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
assert lines[1].startswith("license-tag=v-")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_matrix_fails_without_builds(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
44
|
+
write_repository(tmp_path)
|
|
45
|
+
monkeypatch.chdir(tmp_path)
|
|
46
|
+
|
|
47
|
+
with pytest.raises(SystemExit, match="No projects with builds declared"):
|
|
48
|
+
matrix.build_matrix()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_matrix_project_filter_scopes_to_one_project(
|
|
52
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
53
|
+
) -> None:
|
|
54
|
+
write_repository(tmp_path, builds=["Linux"])
|
|
55
|
+
other = tmp_path / "apps" / "Other"
|
|
56
|
+
(other / "ProjectSettings").mkdir(parents=True)
|
|
57
|
+
(other / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
58
|
+
(other / "unity-devkit.json").write_text(json.dumps({"name": "other", "platforms": {"Linux": {}}}))
|
|
59
|
+
monkeypatch.chdir(tmp_path)
|
|
60
|
+
|
|
61
|
+
matrix.build_matrix(project="other")
|
|
62
|
+
|
|
63
|
+
lines = capsys.readouterr().out.splitlines()
|
|
64
|
+
include = json.loads(lines[0][len("matrix=") :])["include"]
|
|
65
|
+
assert [entry["project-name"] for entry in include] == ["other"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_matrix_project_filter_fails_on_unknown_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
69
|
+
write_repository(tmp_path, builds=["Linux"])
|
|
70
|
+
monkeypatch.chdir(tmp_path)
|
|
71
|
+
|
|
72
|
+
with pytest.raises(SystemExit, match="Unknown project 'missing'"):
|
|
73
|
+
matrix.build_matrix(project="missing")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_matrix_honors_license_cache_tag_override(
|
|
77
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
78
|
+
) -> None:
|
|
79
|
+
write_repository(tmp_path, builds=["Linux"])
|
|
80
|
+
monkeypatch.chdir(tmp_path)
|
|
81
|
+
monkeypatch.setenv("LICENSE_CACHE_TAG", "v-pinned")
|
|
82
|
+
|
|
83
|
+
matrix.build_matrix()
|
|
84
|
+
|
|
85
|
+
lines = capsys.readouterr().out.splitlines()
|
|
86
|
+
assert lines[1] == "license-tag=v-pinned"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_check_matrix_includes_path_only_projects(
|
|
90
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
91
|
+
) -> None:
|
|
92
|
+
write_repository(tmp_path)
|
|
93
|
+
monkeypatch.chdir(tmp_path)
|
|
94
|
+
monkeypatch.delenv("LICENSE_CACHE_TAG", raising=False)
|
|
95
|
+
|
|
96
|
+
matrix.compile_check_matrix()
|
|
97
|
+
|
|
98
|
+
lines = capsys.readouterr().out.splitlines()
|
|
99
|
+
include = json.loads(lines[0][len("matrix=") :])["include"]
|
|
100
|
+
assert include == [{"project-name": "tool", "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3"}]
|
|
101
|
+
assert lines[1].startswith("license-tag=v-")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_check_matrix_project_filter_scopes_to_one_project(
|
|
105
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
106
|
+
) -> None:
|
|
107
|
+
write_repository(tmp_path)
|
|
108
|
+
other = tmp_path / "apps" / "Other"
|
|
109
|
+
(other / "ProjectSettings").mkdir(parents=True)
|
|
110
|
+
(other / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
111
|
+
(other / "unity-devkit.json").write_text(json.dumps({"name": "other"}))
|
|
112
|
+
monkeypatch.chdir(tmp_path)
|
|
113
|
+
|
|
114
|
+
matrix.compile_check_matrix(project="other")
|
|
115
|
+
|
|
116
|
+
lines = capsys.readouterr().out.splitlines()
|
|
117
|
+
include = json.loads(lines[0][len("matrix=") :])["include"]
|
|
118
|
+
assert include == [{"project-name": "other", "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3"}]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from unity_devkit.upm_cache import upm_cache_tag
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def write_packages(project_path: Path, manifest: str, lock: str | None) -> None:
|
|
8
|
+
packages = project_path / "Packages"
|
|
9
|
+
packages.mkdir(parents=True)
|
|
10
|
+
(packages / "manifest.json").write_text(manifest)
|
|
11
|
+
if lock is not None:
|
|
12
|
+
(packages / "packages-lock.json").write_text(lock)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_tag_hashes_both_manifests(tmp_path: Path) -> None:
|
|
16
|
+
write_packages(tmp_path, '{"a":1}', '{"b":2}')
|
|
17
|
+
|
|
18
|
+
expected = hashlib.sha256(b'{"a":1}{"b":2}').hexdigest()
|
|
19
|
+
|
|
20
|
+
assert upm_cache_tag(tmp_path) == expected
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_tag_changes_when_either_manifest_changes(tmp_path: Path) -> None:
|
|
24
|
+
write_packages(tmp_path, '{"a":1}', '{"b":2}')
|
|
25
|
+
before = upm_cache_tag(tmp_path)
|
|
26
|
+
|
|
27
|
+
(tmp_path / "Packages" / "packages-lock.json").write_text('{"b":3}')
|
|
28
|
+
|
|
29
|
+
assert upm_cache_tag(tmp_path) != before
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_tag_treats_missing_lock_as_empty(tmp_path: Path) -> None:
|
|
33
|
+
write_packages(tmp_path, '{"a":1}', None)
|
|
34
|
+
|
|
35
|
+
expected = hashlib.sha256(b'{"a":1}').hexdigest()
|
|
36
|
+
|
|
37
|
+
assert upm_cache_tag(tmp_path) == expected
|
|
@@ -1,268 +0,0 @@
|
|
|
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 discover_projects
|
|
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 (the 'name' field of its 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-internal.yml reference "
|
|
67
|
-
"(owner/repo/.github/workflows/build-unity-internal.yml@sha, or ./.github/workflows/build-unity-internal.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 unity-devkit.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 = discover_projects()
|
|
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
|
-
workflow_call_input_blocks: list[tuple[str, str | None, list[str], str | None]] = [
|
|
164
|
-
("runner-labels", "JSON-encoded runs-on value", ["type: string"], "default: '\"ubuntu-latest\"'"),
|
|
165
|
-
("version", "Version string to stamp; empty builds unversioned", ["type: string"], "default: ''"),
|
|
166
|
-
]
|
|
167
|
-
|
|
168
|
-
lines = [
|
|
169
|
-
header,
|
|
170
|
-
"# edit the class, then regenerate this workflow.",
|
|
171
|
-
"name: Unity Dispatch Build",
|
|
172
|
-
"",
|
|
173
|
-
"on:",
|
|
174
|
-
" workflow_dispatch:",
|
|
175
|
-
" inputs:",
|
|
176
|
-
*indent_lines([line for block in input_blocks for line in build_input_block(block)], 6),
|
|
177
|
-
" workflow_call:",
|
|
178
|
-
" inputs:",
|
|
179
|
-
*indent_lines([line for block in workflow_call_input_blocks for line in build_input_block(block)], 6),
|
|
180
|
-
"",
|
|
181
|
-
"jobs:",
|
|
182
|
-
" build:",
|
|
183
|
-
f" uses: {build_workflow}",
|
|
184
|
-
" with:",
|
|
185
|
-
" runner-labels: ${{ inputs.runner-labels || '\"ubuntu-latest\"' }}",
|
|
186
|
-
" version: ${{ inputs.version || '' }}",
|
|
187
|
-
" environment-preset: ${{ inputs['environment-preset'] || '' }}",
|
|
188
|
-
" development: ${{ inputs.development || false }}",
|
|
189
|
-
]
|
|
190
|
-
if transport_lines:
|
|
191
|
-
lines.append(" environment-fields: |-")
|
|
192
|
-
lines.extend(indent_lines(transport_lines, 8))
|
|
193
|
-
else:
|
|
194
|
-
lines.append(" environment-fields: ''")
|
|
195
|
-
lines.append(" secrets: inherit")
|
|
196
|
-
return "\n".join(lines) + "\n"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def regen_command(project: str, output: str, build_workflow: str) -> str:
|
|
200
|
-
return f"uv run unity-dispatch --project {project} --output {output} --build-workflow {build_workflow}"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def field_input_block(
|
|
204
|
-
field: EnvironmentFieldDump, input_id: str, enums_by_name: dict[str, EnvironmentEnum]
|
|
205
|
-
) -> tuple[str, str | None, list[str], str | None]:
|
|
206
|
-
if field.field_type == "Boolean":
|
|
207
|
-
return (input_id, None, ["type: boolean"], "default: false")
|
|
208
|
-
|
|
209
|
-
if field.field_type in enums_by_name:
|
|
210
|
-
environment_enum = enums_by_name[field.field_type]
|
|
211
|
-
if environment_enum.flags:
|
|
212
|
-
return (
|
|
213
|
-
input_id,
|
|
214
|
-
f"comma-separated: {', '.join(environment_enum.values)}",
|
|
215
|
-
["type: string"],
|
|
216
|
-
'default: ""',
|
|
217
|
-
)
|
|
218
|
-
return (
|
|
219
|
-
input_id,
|
|
220
|
-
None,
|
|
221
|
-
[
|
|
222
|
-
"type: choice",
|
|
223
|
-
"options:",
|
|
224
|
-
*indent_lines([*(f'- "{value}"' for value in ("", *environment_enum.values))], 2),
|
|
225
|
-
],
|
|
226
|
-
'default: ""',
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
if field.field_type == "String":
|
|
230
|
-
return (input_id, None, ["type: string"], 'default: ""')
|
|
231
|
-
|
|
232
|
-
if field.field_type in NUMERIC_FIELD_TYPES:
|
|
233
|
-
return (input_id, None, ["type: number"], None)
|
|
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, input_id: str) -> str:
|
|
242
|
-
guard = "" if field.field_type == "Boolean" else " != ''"
|
|
243
|
-
return (
|
|
244
|
-
"${{ inputs['"
|
|
245
|
-
+ input_id
|
|
246
|
-
+ "']"
|
|
247
|
-
+ guard
|
|
248
|
-
+ " && format('"
|
|
249
|
-
+ field.path
|
|
250
|
-
+ "={0}', inputs['"
|
|
251
|
-
+ input_id
|
|
252
|
-
+ "']) || '' }}"
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def build_input_block(input_block: tuple[str, str | None, list[str], str | None]) -> list[str]:
|
|
257
|
-
input_id, description, type_lines, default_line = input_block
|
|
258
|
-
lines = [f"{input_id}:"]
|
|
259
|
-
if description is not None:
|
|
260
|
-
lines.append(f' description: "{description}"')
|
|
261
|
-
lines.extend(f" {line}" for line in type_lines)
|
|
262
|
-
if default_line is not None:
|
|
263
|
-
lines.append(f" {default_line}")
|
|
264
|
-
return lines
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def indent_lines(lines: list[str], spaces: int) -> list[str]:
|
|
268
|
-
return [f"{' ' * spaces}{line}" if line else line for line in lines]
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
from bashrun.bash import bash
|
|
7
|
-
from pydantic_settings import BaseSettings
|
|
8
|
-
|
|
9
|
-
from ci_devkit.cache import restore, save
|
|
10
|
-
from .license_restore import license_cache_tag
|
|
11
|
-
from ci_devkit.setup import configure_git
|
|
12
|
-
from ci_devkit.setup_oras import install_oras
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class Settings(BaseSettings):
|
|
16
|
-
cache_registry: str
|
|
17
|
-
github_workspace: str
|
|
18
|
-
unity_email: str
|
|
19
|
-
unity_password: str
|
|
20
|
-
unity_serial: str
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
settings = Settings.model_validate({})
|
|
24
|
-
activate_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@activate_app.command()
|
|
28
|
-
def activate_main(oras_push: bool = typer.Option(False, help="Push activated ULF to ORAS cache")) -> None:
|
|
29
|
-
configure_git(settings.github_workspace)
|
|
30
|
-
install_oras()
|
|
31
|
-
|
|
32
|
-
license_directory = Path.home() / ".local" / "share" / "unity3d" / "Unity"
|
|
33
|
-
license_directory.mkdir(parents=True, exist_ok=True)
|
|
34
|
-
|
|
35
|
-
tag = license_cache_tag()
|
|
36
|
-
cache_hit = False
|
|
37
|
-
if oras_push:
|
|
38
|
-
cache_hit = restore(settings.cache_registry, "unity-license", tag, license_directory)
|
|
39
|
-
|
|
40
|
-
if not cache_hit:
|
|
41
|
-
bash(
|
|
42
|
-
f'unity-editor -batchmode -nographics -quit -serial "{settings.unity_serial}"'
|
|
43
|
-
f' -username "{settings.unity_email}" -password "{settings.unity_password}" -logFile /dev/stdout'
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
if oras_push and not cache_hit:
|
|
47
|
-
save(settings.cache_registry, "unity-license", tag, license_directory, ["Unity_lic.ulf"])
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from datetime import UTC, datetime
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import typer
|
|
8
|
-
from pydantic_settings import BaseSettings
|
|
9
|
-
|
|
10
|
-
from ci_devkit.cache import restore
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Settings(BaseSettings):
|
|
14
|
-
cache_registry: str
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
tag_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@tag_app.command()
|
|
21
|
-
def tag_main() -> None:
|
|
22
|
-
print(license_cache_tag())
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def restore_license() -> None:
|
|
26
|
-
settings = Settings.model_validate({})
|
|
27
|
-
license_directory = Path.home() / ".local" / "share" / "unity3d" / "Unity"
|
|
28
|
-
license_directory.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
restore(settings.cache_registry, "unity-license", license_cache_tag(), license_directory, required=True)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# LICENSE_CACHE_TAG env pins the tag for a whole CI run; without it, each call
|
|
33
|
-
# re-reads "now UTC" and a run straddling midnight save/restore-misses itself.
|
|
34
|
-
def license_cache_tag() -> str:
|
|
35
|
-
override = os.environ.get("LICENSE_CACHE_TAG")
|
|
36
|
-
if override:
|
|
37
|
-
return override
|
|
38
|
-
return f"v-{datetime.now(UTC).strftime('%Y-%m-%d')}"
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from .projects import discover_projects
|
|
5
|
-
from .player_build import LICENSE_IMAGE_MODULE, PLATFORM_CONFIGS, UNITYCI_IMAGE_REVISION, read_editor_version
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def build_matrix() -> None:
|
|
9
|
-
projects = discover_projects()
|
|
10
|
-
matrix: list[dict[str, str]] = []
|
|
11
|
-
editor_versions: set[str] = set()
|
|
12
|
-
|
|
13
|
-
for name, project in projects.items():
|
|
14
|
-
if not project.builds:
|
|
15
|
-
continue
|
|
16
|
-
version = read_editor_version(project.path)
|
|
17
|
-
editor_versions.add(version)
|
|
18
|
-
for platform in project.builds:
|
|
19
|
-
image_module = PLATFORM_CONFIGS[platform]["unityci_image_module"]
|
|
20
|
-
matrix.append({
|
|
21
|
-
"project": str(project.path.relative_to(Path.cwd())),
|
|
22
|
-
"project-name": name,
|
|
23
|
-
"cache-key": name.lower(),
|
|
24
|
-
"platform": platform,
|
|
25
|
-
"unityci-image-module": image_module,
|
|
26
|
-
"editor-image": f"unityci/editor:{version}-{image_module}-{UNITYCI_IMAGE_REVISION}",
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
if not editor_versions:
|
|
30
|
-
raise SystemExit(
|
|
31
|
-
"No projects with builds declared — build-unity-matrix needs at least one unity-devkit.json with a non-empty 'platforms' map"
|
|
32
|
-
)
|
|
33
|
-
license_version = max(editor_versions)
|
|
34
|
-
print(f"matrix={json.dumps({'include': matrix})}")
|
|
35
|
-
print(f"license-image=unityci/editor:{license_version}-{LICENSE_IMAGE_MODULE}-{UNITYCI_IMAGE_REVISION}")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def compile_check_matrix() -> None:
|
|
39
|
-
projects = discover_projects()
|
|
40
|
-
matrix: list[dict[str, str]] = []
|
|
41
|
-
editor_versions: set[str] = set()
|
|
42
|
-
|
|
43
|
-
for name, project in projects.items():
|
|
44
|
-
version = read_editor_version(project.path)
|
|
45
|
-
editor_versions.add(version)
|
|
46
|
-
matrix.append({
|
|
47
|
-
"project-name": name,
|
|
48
|
-
"editor-image": f"unityci/editor:{version}-{LICENSE_IMAGE_MODULE}-{UNITYCI_IMAGE_REVISION}",
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
print(f"matrix={json.dumps({'include': matrix})}")
|
|
52
|
-
print(f"license-image=unityci/editor:{max(editor_versions)}-{LICENSE_IMAGE_MODULE}-{UNITYCI_IMAGE_REVISION}")
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
import yaml
|
|
6
|
-
|
|
7
|
-
from unity_devkit.dispatch_workflow_generator import (
|
|
8
|
-
EnvironmentDump,
|
|
9
|
-
EnvironmentFieldDump,
|
|
10
|
-
extract_environment_dump,
|
|
11
|
-
render_dispatch_workflow,
|
|
12
|
-
)
|
|
13
|
-
from unity_devkit.player_build import parse_environment_fields
|
|
14
|
-
|
|
15
|
-
FIXTURE_DUMP = Path(__file__).parent / "fixtures" / "environment-dump.json"
|
|
16
|
-
PINNED_BUILD_WORKFLOW = "outernet-foundation/unity-devkit/.github/workflows/build-unity-internal.yml@cfd487e0e19b3d98046a2680a210137ec0d32832"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def load_fixture_dump() -> EnvironmentDump:
|
|
20
|
-
return EnvironmentDump.model_validate(json.loads(FIXTURE_DUMP.read_text(encoding="utf-8")))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def render_fixture_workflow() -> str:
|
|
24
|
-
return render_dispatch_workflow(
|
|
25
|
-
load_fixture_dump(),
|
|
26
|
-
project="PlayerBuild",
|
|
27
|
-
output=".github/workflows/build.yml",
|
|
28
|
-
build_workflow=PINNED_BUILD_WORKFLOW,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_render_modes_are_deduped_into_the_preset_choice() -> None:
|
|
33
|
-
workflow = render_fixture_workflow()
|
|
34
|
-
|
|
35
|
-
assert "configMode:" not in workflow
|
|
36
|
-
assert 'options:\n - ""\n - "Airgapped"' in workflow
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_render_defaults_the_preset_choice_to_the_first_map_key() -> None:
|
|
40
|
-
document = yaml.safe_load(render_fixture_workflow().replace("${{", "__EXPR_OPEN__").replace("}}", "__EXPR_CLOSE__"))
|
|
41
|
-
|
|
42
|
-
trigger = document.get("on", document[True])
|
|
43
|
-
preset = trigger["workflow_dispatch"]["inputs"]["environment-preset"]
|
|
44
|
-
assert preset["options"] == ["", "Airgapped"]
|
|
45
|
-
assert preset["default"] == "Airgapped"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_render_states_the_override_law_once_on_the_preset_input() -> None:
|
|
49
|
-
workflow = render_fixture_workflow()
|
|
50
|
-
|
|
51
|
-
assert "environment field" not in workflow
|
|
52
|
-
assert "empty keeps the preset value" not in workflow
|
|
53
|
-
assert "checked overrides the preset value" not in workflow
|
|
54
|
-
assert workflow.count("description:") == 5
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_render_maps_field_types_to_dispatch_inputs_without_descriptions() -> None:
|
|
58
|
-
workflow = render_fixture_workflow()
|
|
59
|
-
|
|
60
|
-
assert "\n username:\n type: string" in workflow
|
|
61
|
-
assert "\n verboseLogging:\n type: boolean" in workflow
|
|
62
|
-
assert "\n pollIntervalSeconds:\n type: number" in workflow
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_render_uses_leaf_names_as_input_ids_when_unique() -> None:
|
|
66
|
-
workflow = render_fixture_workflow()
|
|
67
|
-
|
|
68
|
-
assert "\n apiUrl:" in workflow
|
|
69
|
-
assert "\n portNumber:" in workflow
|
|
70
|
-
assert "localConfig-apiUrl" not in workflow
|
|
71
|
-
assert "${{ inputs['apiUrl'] != '' && format('localConfig.apiUrl={0}', inputs['apiUrl']) || '' }}" in workflow
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_render_falls_back_to_dashed_paths_on_leaf_collision() -> None:
|
|
75
|
-
dump = load_fixture_dump()
|
|
76
|
-
dump.fields.append(EnvironmentFieldDump(path="remoteConfig.apiUrl", field_type="String"))
|
|
77
|
-
|
|
78
|
-
workflow = render_dispatch_workflow(
|
|
79
|
-
dump, project="PlayerBuild", output=".github/workflows/build.yml", build_workflow=PINNED_BUILD_WORKFLOW
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
assert "\n apiUrl:" not in workflow
|
|
83
|
-
assert "\n localConfig-apiUrl:" in workflow
|
|
84
|
-
assert "\n remoteConfig-apiUrl:" in workflow
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def test_render_falls_back_to_dashed_paths_on_control_id_collision() -> None:
|
|
88
|
-
dump = load_fixture_dump()
|
|
89
|
-
dump.fields.append(EnvironmentFieldDump(path="misc.development", field_type="Boolean"))
|
|
90
|
-
|
|
91
|
-
workflow = render_dispatch_workflow(
|
|
92
|
-
dump, project="PlayerBuild", output=".github/workflows/build.yml", build_workflow=PINNED_BUILD_WORKFLOW
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
assert "\n misc-development:" in workflow
|
|
96
|
-
assert workflow.split("jobs:")[0].count("\n development:") == 1
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_render_flags_enum_is_text_not_choice() -> None:
|
|
100
|
-
workflow = render_fixture_workflow()
|
|
101
|
-
|
|
102
|
-
assert 'description: "comma-separated: None, Capture, Stream, Analyze"' in workflow
|
|
103
|
-
assert "\n capabilities:\n description:" in workflow
|
|
104
|
-
assert "\n capabilities:\n type: string" not in workflow
|
|
105
|
-
assert "type: choice" in workflow
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def test_render_transports_booleans_only_when_checked() -> None:
|
|
109
|
-
workflow = render_fixture_workflow()
|
|
110
|
-
|
|
111
|
-
assert "${{ inputs['verboseLogging'] && format('verboseLogging={0}', inputs['verboseLogging']) || '' }}" in workflow
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_render_transports_empty_as_unset() -> None:
|
|
115
|
-
workflow = render_fixture_workflow()
|
|
116
|
-
|
|
117
|
-
assert "${{ inputs['username'] != '' && format('username={0}', inputs['username']) || '' }}" in workflow
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def test_render_rejects_unknown_field_type() -> None:
|
|
121
|
-
dump = load_fixture_dump()
|
|
122
|
-
dump.fields.append(EnvironmentFieldDump(path="weird", field_type="Byte[]"))
|
|
123
|
-
|
|
124
|
-
with pytest.raises(SystemExit, match="no dispatch input type maps"):
|
|
125
|
-
render_dispatch_workflow(
|
|
126
|
-
dump, project="PlayerBuild", output=".github/workflows/build.yml", build_workflow=PINNED_BUILD_WORKFLOW
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_render_enforces_the_dispatch_input_cap() -> None:
|
|
131
|
-
dump = load_fixture_dump()
|
|
132
|
-
dump.fields.extend(EnvironmentFieldDump(path=f"extra{index}", field_type="String") for index in range(25))
|
|
133
|
-
|
|
134
|
-
with pytest.raises(SystemExit, match="cap is 25"):
|
|
135
|
-
render_dispatch_workflow(
|
|
136
|
-
dump, project="PlayerBuild", output=".github/workflows/build.yml", build_workflow=PINNED_BUILD_WORKFLOW
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def test_render_is_deterministic() -> None:
|
|
141
|
-
assert render_fixture_workflow() == render_fixture_workflow()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def test_rendered_workflow_is_valid_yaml() -> None:
|
|
145
|
-
document = yaml.safe_load(render_fixture_workflow().replace("${{", "__EXPR_OPEN__").replace("}}", "__EXPR_CLOSE__"))
|
|
146
|
-
|
|
147
|
-
trigger = document.get("on", document[True])
|
|
148
|
-
inputs = trigger["workflow_dispatch"]["inputs"]
|
|
149
|
-
assert list(inputs) == [
|
|
150
|
-
"environment-preset",
|
|
151
|
-
"development",
|
|
152
|
-
"username",
|
|
153
|
-
"password",
|
|
154
|
-
"verboseLogging",
|
|
155
|
-
"pollIntervalSeconds",
|
|
156
|
-
"capabilities",
|
|
157
|
-
"apiUrl",
|
|
158
|
-
"portNumber",
|
|
159
|
-
]
|
|
160
|
-
call_inputs = trigger["workflow_call"]["inputs"]
|
|
161
|
-
assert list(call_inputs) == ["runner-labels", "version"]
|
|
162
|
-
assert call_inputs["runner-labels"]["type"] == "string"
|
|
163
|
-
assert call_inputs["runner-labels"]["default"] == '"ubuntu-latest"'
|
|
164
|
-
assert call_inputs["version"]["type"] == "string"
|
|
165
|
-
assert call_inputs["version"]["default"] == ""
|
|
166
|
-
assert document["jobs"]["build"]["uses"] == PINNED_BUILD_WORKFLOW
|
|
167
|
-
assert document["jobs"]["build"]["secrets"] == "inherit"
|
|
168
|
-
with_block = document["jobs"]["build"]["with"]
|
|
169
|
-
assert with_block["runner-labels"] == "__EXPR_OPEN__ inputs.runner-labels || '\"ubuntu-latest\"' __EXPR_CLOSE__"
|
|
170
|
-
assert with_block["version"] == "__EXPR_OPEN__ inputs.version || '' __EXPR_CLOSE__"
|
|
171
|
-
assert with_block["environment-preset"] == "__EXPR_OPEN__ inputs['environment-preset'] || '' __EXPR_CLOSE__"
|
|
172
|
-
assert with_block["development"] == "__EXPR_OPEN__ inputs.development || false __EXPR_CLOSE__"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def test_render_with_block_uses_or_defaults_for_dual_trigger_support() -> None:
|
|
176
|
-
workflow = render_fixture_workflow()
|
|
177
|
-
|
|
178
|
-
assert "runner-labels: ${{ inputs.runner-labels || '\"ubuntu-latest\"' }}" in workflow
|
|
179
|
-
assert "version: ${{ inputs.version || '' }}" in workflow
|
|
180
|
-
assert "environment-preset: ${{ inputs['environment-preset'] || '' }}" in workflow
|
|
181
|
-
assert "development: ${{ inputs.development || false }}" in workflow
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def test_render_header_embeds_the_full_regen_command() -> None:
|
|
185
|
-
header = render_fixture_workflow().splitlines()[0]
|
|
186
|
-
|
|
187
|
-
assert (
|
|
188
|
-
f"uv run unity-dispatch --project PlayerBuild --output .github/workflows/build.yml "
|
|
189
|
-
f"--build-workflow {PINNED_BUILD_WORKFLOW}" in header
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def test_extract_from_log_text(tmp_path: Path) -> None:
|
|
194
|
-
log_path = tmp_path / "editor.log"
|
|
195
|
-
log_path.write_text(
|
|
196
|
-
"Refreshing native plugins\n"
|
|
197
|
-
'{"class_name":"Outernet.FixtureEnv","mode_field":"configMode","target_path":"t","presets":{},'
|
|
198
|
-
'"enums":[],"fields":[]}\n'
|
|
199
|
-
"at Outernet.EnvironmentConfig.Dump () [0x00000]\n",
|
|
200
|
-
encoding="utf-8",
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
dump = extract_environment_dump(log_path)
|
|
204
|
-
|
|
205
|
-
assert dump.class_name == "Outernet.FixtureEnv"
|
|
206
|
-
assert dump.presets == {}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def test_extract_fails_loudly_without_payload(tmp_path: Path) -> None:
|
|
210
|
-
log_path = tmp_path / "editor.log"
|
|
211
|
-
log_path.write_text("nothing to see here\n", encoding="utf-8")
|
|
212
|
-
|
|
213
|
-
with pytest.raises(SystemExit, match="No environment dump JSON"):
|
|
214
|
-
extract_environment_dump(log_path)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def test_parse_environment_fields_splits_on_first_equals() -> None:
|
|
218
|
-
fields = parse_environment_fields(["username=bot", "localConfig.apiUrl=https://x/?a=b", "", " "])
|
|
219
|
-
|
|
220
|
-
assert fields == {"username": "bot", "localConfig.apiUrl": "https://x/?a=b"}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def test_parse_environment_fields_rejects_bare_path() -> None:
|
|
224
|
-
with pytest.raises(SystemExit, match="expected path=value"):
|
|
225
|
-
parse_environment_fields(["username"])
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from unity_devkit import matrix
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def write_repository(tmp_path: Path, builds: list[str] | None = None) -> None:
|
|
10
|
-
project = tmp_path / "apps" / "Tool"
|
|
11
|
-
(project / "ProjectSettings").mkdir(parents=True)
|
|
12
|
-
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
13
|
-
config: dict[str, object] = {"name": "tool"}
|
|
14
|
-
if builds is not None:
|
|
15
|
-
config["platforms"] = {platform: {} for platform in builds}
|
|
16
|
-
(project / "unity-devkit.json").write_text(json.dumps(config))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_matrix_emits_entries_from_catalog(
|
|
20
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
21
|
-
) -> None:
|
|
22
|
-
write_repository(tmp_path, builds=["Linux"])
|
|
23
|
-
monkeypatch.chdir(tmp_path)
|
|
24
|
-
|
|
25
|
-
matrix.build_matrix()
|
|
26
|
-
|
|
27
|
-
lines = capsys.readouterr().out.splitlines()
|
|
28
|
-
include = json.loads(lines[0][len("matrix=") :])["include"]
|
|
29
|
-
assert include == [
|
|
30
|
-
{
|
|
31
|
-
"project": str(Path("apps") / "Tool"),
|
|
32
|
-
"project-name": "tool",
|
|
33
|
-
"cache-key": "tool",
|
|
34
|
-
"platform": "Linux",
|
|
35
|
-
"unityci-image-module": "linux-il2cpp",
|
|
36
|
-
"editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3",
|
|
37
|
-
}
|
|
38
|
-
]
|
|
39
|
-
assert lines[1] == "license-image=unityci/editor:6000.0.66f1-linux-il2cpp-3"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_matrix_fails_without_builds(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
43
|
-
write_repository(tmp_path)
|
|
44
|
-
monkeypatch.chdir(tmp_path)
|
|
45
|
-
|
|
46
|
-
with pytest.raises(SystemExit, match="No projects with builds declared"):
|
|
47
|
-
matrix.build_matrix()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_check_matrix_includes_path_only_projects(
|
|
51
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
52
|
-
) -> None:
|
|
53
|
-
write_repository(tmp_path)
|
|
54
|
-
monkeypatch.chdir(tmp_path)
|
|
55
|
-
|
|
56
|
-
matrix.compile_check_matrix()
|
|
57
|
-
|
|
58
|
-
lines = capsys.readouterr().out.splitlines()
|
|
59
|
-
include = json.loads(lines[0][len("matrix=") :])["include"]
|
|
60
|
-
assert include == [{"project-name": "tool", "editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3"}]
|
|
61
|
-
assert lines[1] == "license-image=unityci/editor:6000.0.66f1-linux-il2cpp-3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|