unity-devkit 0.1.0__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from pydantic_settings import BaseSettings
10
+
11
+ from .cache import restore, save
12
+ from .ci_step import ci_step
13
+ from .license_restore import restore_license
14
+ from .setup import configure_git, install_dotnet
15
+ from .setup_oras import install_oras
16
+ from .unity import prepare_unity_project, resolve_unity_build, run_unity_batchmode
17
+ from .git_tags import get_latest_tag_version
18
+
19
+
20
+ class Settings(BaseSettings):
21
+ github_workspace: str
22
+
23
+
24
+ settings = Settings.model_validate({})
25
+
26
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
27
+
28
+
29
+ @app.command()
30
+ def main(
31
+ project: str = typer.Option(help="Project name"),
32
+ project_path: Path = typer.Option(help="Path to Unity project"),
33
+ platform: str = typer.Option(help="Target platform"),
34
+ cache_key: str = typer.Option(help="Cache key prefix"),
35
+ run_number: int = typer.Option(0, help="CI run number"),
36
+ branch: str = typer.Option("dev", help="Git branch name"),
37
+ registry: str = typer.Option(help="OCI registry path"),
38
+ build_env: str = typer.Option(
39
+ "", help="Newline-separated KEY=VALUE pairs injected into the Unity build process environment"
40
+ ),
41
+ ) -> None:
42
+ for line in build_env.splitlines():
43
+ entry = line.strip()
44
+ if not entry:
45
+ continue
46
+ key, separator, value = entry.partition("=")
47
+ if not separator:
48
+ raise SystemExit(f"Invalid --build-env entry (expected KEY=VALUE): {entry!r}")
49
+ os.environ[key.strip()] = value.strip()
50
+
51
+ with ci_step("Setup"):
52
+ configure_git(settings.github_workspace)
53
+ install_dotnet("8.0")
54
+ install_oras()
55
+ restore_license()
56
+
57
+ branch_slug = branch.replace("/", "-")
58
+ tag = f"{cache_key}-{platform}-{branch_slug}"
59
+ fallback_branch = "dev"
60
+ fallback_tags = [f"{cache_key}-{platform}-{fallback_branch}"] if branch_slug != fallback_branch else None
61
+
62
+ with ci_step("Restore library cache"):
63
+ restore(registry, "unity-library", tag, Path("."), fallback_tags=fallback_tags)
64
+
65
+ with ci_step("Prepare build"):
66
+ project_config, build_flag, execute_method = resolve_unity_build(project, platform)
67
+ unity_project_path = project_config.path
68
+
69
+ with ci_step("Prepare project"):
70
+ prepare_unity_project(unity_project_path)
71
+
72
+ with ci_step(f"Build {unity_project_path.name} [{platform}]"):
73
+ tag_prefix = project_config.tag_prefix
74
+ if tag_prefix:
75
+ version = get_latest_tag_version(f"{tag_prefix}-v") or "0.0.0"
76
+ full_version = f"{version}-dev+{run_number}" if branch != "main" else f"{version}+{run_number}"
77
+ version_file = unity_project_path / ".build-version.json"
78
+ version_file.write_text(json.dumps({"version": full_version, "runNumber": run_number}))
79
+ print(f"Wrote version {full_version} (bundleVersionCode={run_number}) to {version_file}")
80
+
81
+ run_unity_batchmode(unity_project_path, f"{build_flag} -executeMethod {execute_method}", nographics=False)
82
+
83
+ with ci_step("Save library cache"):
84
+ # PackageCache (~1.6 GiB) is redundant with the shared UPM cache at ~/.cache/Unity/upm/
85
+ package_cache = project_path / "Library" / "PackageCache"
86
+ if package_cache.exists():
87
+ shutil.rmtree(package_cache)
88
+
89
+ save(registry, "unity-library", tag, Path("."), [f"{project_path}/Library/"])
90
+
91
+ with ci_step("Collect build artifacts"):
92
+ build_directory = project_path / "Build"
93
+ if build_directory.is_dir():
94
+ artifact_directory = Path("/tmp/unity-builds")
95
+ artifact_directory.mkdir(parents=True, exist_ok=True)
96
+ if platform == "linux64":
97
+ shutil.copytree(build_directory, artifact_directory, dirs_exist_ok=True)
98
+ else:
99
+ for file in build_directory.rglob("*"):
100
+ if file.suffix in {".apk", ".exe"}:
101
+ shutil.copy2(file, artifact_directory / file.name)
unity_devkit/cache.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import shutil
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+ from subprocess import CalledProcessError
9
+
10
+ from bashrun import bash, bash_check, bash_pipe
11
+
12
+
13
+ def _posix(path: Path) -> str:
14
+ return path.as_posix() if platform.system() == "Windows" else str(path)
15
+
16
+
17
+ def restore(
18
+ registry: str,
19
+ name: str,
20
+ tag: str,
21
+ target_directory: Path,
22
+ *,
23
+ required: bool = False,
24
+ fallback_tags: list[str] | None = None,
25
+ ) -> bool:
26
+ # OCI repository names must be lowercase; GitHub repository names preserve case
27
+ registry = registry.lower()
28
+ staging = Path(tempfile.gettempdir()) / "cache"
29
+
30
+ for candidate_tag in [tag, *(fallback_tags or [])]:
31
+ if candidate_tag != tag:
32
+ print(f"Falling back to {name}:{candidate_tag}")
33
+
34
+ reference = f"{registry}/{name}:{candidate_tag}"
35
+ staging.mkdir(parents=True, exist_ok=True)
36
+
37
+ if not bash_check(f"oras pull {reference} -o {staging}"):
38
+ shutil.rmtree(staging, ignore_errors=True)
39
+ continue
40
+
41
+ archive = staging / f"{name}.tar.zst"
42
+ try:
43
+ if platform.system() == "Windows":
44
+ bash_pipe(f"zstd -d {_posix(archive)} --stdout", f"tar -xf - -C {_posix(target_directory)}")
45
+ else:
46
+ bash(f"tar -xf {archive} -C {target_directory}")
47
+ except CalledProcessError:
48
+ print(f"WARNING: Cache extraction failed for {name}:{candidate_tag}, trying next")
49
+ shutil.rmtree(staging, ignore_errors=True)
50
+ continue
51
+
52
+ shutil.rmtree(staging, ignore_errors=True)
53
+ print(f"Cache hit: {name}:{candidate_tag}")
54
+ return True
55
+
56
+ if required:
57
+ print(f"FATAL: Required cache missing: {registry}/{name}:{tag}")
58
+ sys.exit(1)
59
+
60
+ print(f"Cache miss: {name}")
61
+ return False
62
+
63
+
64
+ def save(registry: str, name: str, tag: str, source_directory: Path, paths: list[str]) -> None:
65
+ # OCI repository names must be lowercase; GitHub repository names preserve case
66
+ registry = registry.lower()
67
+ resolved: list[str] = []
68
+ for pattern in paths:
69
+ if any(character in pattern for character in "*?["):
70
+ matches = sorted(source_directory.glob(pattern))
71
+ resolved.extend(str(match.relative_to(source_directory)) for match in matches)
72
+ else:
73
+ resolved.append(pattern)
74
+
75
+ staging = Path(tempfile.gettempdir()) / "cache"
76
+ staging.mkdir(parents=True, exist_ok=True)
77
+ archive_name = f"{name}.tar.zst"
78
+ archive_path = staging / archive_name
79
+
80
+ joined = " ".join(resolved)
81
+ if platform.system() == "Windows":
82
+ bash_pipe(f"tar -cf - {joined}", f"zstd -o {_posix(archive_path)}", cwd=source_directory)
83
+ else:
84
+ bash(f"tar --zstd -cf {archive_path} {joined}", cwd=source_directory)
85
+
86
+ reference = f"{registry}/{name}:{tag}"
87
+ bash(f"oras push {reference} {archive_name}:application/vnd.unity-devkit.cache.v1+zstd", cwd=staging)
88
+ archive_path.unlink()
89
+ print(f"Saved cache: {reference}")
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+
7
+ from pydantic_settings import BaseSettings
8
+
9
+
10
+ class Settings(BaseSettings):
11
+ github_step_summary: str | None = None
12
+
13
+
14
+ settings = Settings.model_validate({})
15
+ _summary_initialized = False
16
+
17
+
18
+ def _format_duration(seconds: float) -> str:
19
+ if seconds < 60:
20
+ return f"{seconds:.1f}s"
21
+ minutes = int(seconds // 60)
22
+ remaining = seconds % 60
23
+ return f"{minutes}m {remaining:.0f}s"
24
+
25
+
26
+ def _write_summary(label: str, duration: float, *, failed: bool) -> None:
27
+ global _summary_initialized
28
+ summary_path = settings.github_step_summary
29
+ if not summary_path:
30
+ return
31
+ with open(summary_path, "a") as file:
32
+ if not _summary_initialized:
33
+ file.write("| Step | Duration |\n|---|---|\n")
34
+ _summary_initialized = True
35
+ status = " :x:" if failed else ""
36
+ file.write(f"| {label}{status} | {_format_duration(duration)} |\n")
37
+
38
+
39
+ @contextmanager
40
+ def ci_step(label: str) -> Generator[None]:
41
+ print(f"::group::{label}", flush=True)
42
+ start = time.monotonic()
43
+ failed = False
44
+ try:
45
+ yield
46
+ except BaseException:
47
+ failed = True
48
+ raise
49
+ finally:
50
+ duration = time.monotonic() - start
51
+ print("::endgroup::", flush=True)
52
+ _write_summary(label, duration, failed=failed)
@@ -0,0 +1,46 @@
1
+ from pathlib import Path
2
+ from typing import Annotated
3
+
4
+ import typer
5
+
6
+ from .unity import prepare_unity_project, resolve_unity_build, run_unity_batchmode
7
+
8
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
9
+
10
+
11
+ def build_unity_project(project: str, build: str) -> list[Path]:
12
+ project_config, build_flag, execute_method = resolve_unity_build(project, build)
13
+ project_path = project_config.path
14
+ prepare_unity_project(project_path)
15
+
16
+ build_directory = project_path / "Build"
17
+ before = snapshot_artifacts(build_directory)
18
+
19
+ run_unity_batchmode(project_path, f"{build_flag} -executeMethod {execute_method}", nographics=False)
20
+
21
+ after = snapshot_artifacts(build_directory)
22
+ produced = sorted(path for path, modification_time in after.items() if before.get(path) != modification_time)
23
+ if not produced:
24
+ raise SystemExit(
25
+ "Unity exited 0 but no .apk/.exe under Build/ was produced or updated — "
26
+ "the incremental build served a stale artifact. Delete the existing "
27
+ "output under Build/ and the project's Library/Bee/.../build/ tree, then retry."
28
+ )
29
+ return produced
30
+
31
+
32
+ @app.command()
33
+ def compile_unity(
34
+ project: Annotated[str, typer.Option(help="Unity project name (directory containing unity-build.json)")],
35
+ build: Annotated[str, typer.Option(help="Build target from the project's builds list (e.g. android-mobile)")],
36
+ ) -> None:
37
+ for artifact in build_unity_project(project, build):
38
+ print(f"Built: {artifact}")
39
+
40
+
41
+ def snapshot_artifacts(build_directory: Path) -> dict[Path, int]:
42
+ if not build_directory.is_dir():
43
+ return {}
44
+ return {
45
+ path: path.stat().st_mtime_ns for suffix in (".apk", ".exe") for path in build_directory.rglob(f"*{suffix}")
46
+ }
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from bashrun import bash, bash_check, bash_output
6
+
7
+
8
+ def get_latest_tag_version(prefix: str) -> str | None:
9
+ output = bash_output(f'git tag --list "{prefix}*" --sort=-v:refname').strip()
10
+ if not output:
11
+ return None
12
+ latest_tag = output.splitlines()[0]
13
+ return latest_tag[len(prefix) :]
14
+
15
+
16
+ def has_changes_since_tag(tag: str | None, path: Path) -> bool:
17
+ if tag is None:
18
+ return True
19
+ return not bash_check(f"git diff --quiet {tag} HEAD -- {path}")
20
+
21
+
22
+ def create_and_push_tag(tag: str) -> None:
23
+ bash(f"git tag {tag}")
24
+ bash(f"git push origin {tag}")
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+ from bashrun import bash, bash_check, bash_handoff, bash_output
10
+
11
+ from .compile_unity import build_unity_project
12
+ from .projects import UnityProject, load_unity_projects
13
+
14
+ INSTALLABLE_TARGETS = {"android-mobile", "magicleap", "linux64"}
15
+ ADB_TARGETS = {"android-mobile", "magicleap"}
16
+ CACHE_ROOT = Path.home() / ".unity-devkit" / "builds"
17
+
18
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
19
+
20
+
21
+ def _resolve_project(projects: dict[str, UnityProject], name: str) -> str:
22
+ for project_name in projects:
23
+ if project_name.lower() == name.lower():
24
+ return project_name
25
+ valid = ", ".join(projects.keys())
26
+ raise typer.BadParameter(f"Unknown project '{name}'. Valid projects: {valid}")
27
+
28
+
29
+ def _resolve_target(project_config: UnityProject, project_name: str, target: str | None) -> str:
30
+ installable = [build for build in (project_config.builds or []) if build in INSTALLABLE_TARGETS]
31
+ if target is None:
32
+ if len(installable) == 1:
33
+ return installable[0]
34
+ valid = ", ".join(installable) if installable else "(none)"
35
+ raise typer.BadParameter(
36
+ f"--target is required for {project_name} (multiple installable targets). Valid targets: {valid}"
37
+ )
38
+ for build in installable:
39
+ if build.lower() == target.lower():
40
+ return build
41
+ valid = ", ".join(installable)
42
+ raise typer.BadParameter(f"No installable target '{target}' for {project_name}. Valid targets: {valid}")
43
+
44
+
45
+ def _current_git_branch() -> str:
46
+ branch = bash_output("git rev-parse --abbrev-ref HEAD").strip()
47
+ if branch == "HEAD":
48
+ raise typer.BadParameter("HEAD is detached; pass --branch explicitly")
49
+ return branch
50
+
51
+
52
+ def _find_run_id(artifact_name: str, branch: str) -> str:
53
+ owner_repo = bash_output("gh repo view --json nameWithOwner --jq .nameWithOwner").strip()
54
+ output = bash_output(
55
+ f"gh api repos/{owner_repo}/actions/artifacts --method GET -f name={artifact_name} -f per_page=10 --jq .artifacts"
56
+ )
57
+ artifacts: list[dict[str, Any]] = json.loads(output)
58
+ for artifact in artifacts:
59
+ if artifact["workflow_run"]["head_branch"] == branch:
60
+ return str(artifact["workflow_run"]["id"])
61
+ print(f"No artifact '{artifact_name}' found on branch '{branch}'")
62
+ raise SystemExit(1)
63
+
64
+
65
+ def _download_artifact(run_id: str, artifact_name: str) -> Path:
66
+ cache_path = CACHE_ROOT / run_id / artifact_name
67
+ if cache_path.is_dir() and any(cache_path.iterdir()):
68
+ print(f"Using cached artifact: {cache_path}")
69
+ return cache_path
70
+ cache_path.mkdir(parents=True, exist_ok=True)
71
+ bash(f"gh run download {run_id} --name {artifact_name} --dir {cache_path}")
72
+ return cache_path
73
+
74
+
75
+ def _find_linux_executable(artifact_path: Path) -> Path:
76
+ for item in artifact_path.iterdir():
77
+ if item.is_file() and (artifact_path / f"{item.stem}_Data").is_dir():
78
+ return item
79
+ print("No linux64 executable found in artifact (expected a file with a matching _Data/ directory)")
80
+ raise SystemExit(1)
81
+
82
+
83
+ @app.command()
84
+ def main(
85
+ project: Annotated[str, typer.Option("--project", "-p", help="Unity project name")],
86
+ target: Annotated[
87
+ str | None,
88
+ typer.Option("--target", "-t", help="Device target (android-mobile, magicleap, linux64)"),
89
+ ] = None,
90
+ branch: Annotated[
91
+ str | None,
92
+ typer.Option("--branch", "-b", help="Branch to find latest successful run (default: current git branch)"),
93
+ ] = None,
94
+ run: Annotated[int | None, typer.Option("--run", "-r", help="Specific GitHub Actions run ID")] = None,
95
+ serial: Annotated[str | None, typer.Option("--serial", "-s", help="adb device serial")] = None,
96
+ no_grant_permissions: Annotated[
97
+ bool,
98
+ typer.Option(
99
+ "--no-grant-permissions",
100
+ help=(
101
+ "Skip the post-install `adb shell pm grant` calls listed under `grant_permissions` "
102
+ "for the project in its unity-build.json manifest. Permissions are granted by default."
103
+ ),
104
+ ),
105
+ ] = False,
106
+ build_locally: Annotated[
107
+ bool,
108
+ typer.Option(
109
+ "--build",
110
+ "-B",
111
+ help=(
112
+ "Compile the project locally via `compile-unity` and install the produced APK / "
113
+ "linux executable. Skips the GitHub Actions artifact lookup; --branch / --run are ignored."
114
+ ),
115
+ ),
116
+ ] = False,
117
+ ) -> None:
118
+ projects = load_unity_projects()
119
+ project_name = _resolve_project(projects, project)
120
+ target_name = _resolve_target(projects[project_name], project_name, target)
121
+
122
+ if serial and target_name not in ADB_TARGETS:
123
+ print(f"Warning: --serial is ignored for target '{target_name}'")
124
+
125
+ if build_locally:
126
+ if branch or run:
127
+ print("Warning: --branch / --run are ignored when --build is set")
128
+ produced = build_unity_project(project_name, target_name)
129
+ apks = [path for path in produced if path.suffix == ".apk"]
130
+ executables = [path for path in produced if path.suffix == ".exe"]
131
+ else:
132
+ artifact_name = f"{project_name}-{target_name}"
133
+ resolved_branch = branch or _current_git_branch()
134
+ run_id = str(run) if run else _find_run_id(artifact_name, resolved_branch)
135
+ print(f"Run: {run_id}")
136
+ print(f"Artifact: {artifact_name}")
137
+ download_path = _download_artifact(run_id, artifact_name)
138
+ apks = sorted(download_path.rglob("*.apk"))
139
+ executables = [_find_linux_executable(download_path)] if target_name == "linux64" else []
140
+
141
+ if target_name in ADB_TARGETS:
142
+ if not apks:
143
+ print("No .apk found in install source")
144
+ raise SystemExit(1)
145
+
146
+ print(f"Installing: {apks[0].name}")
147
+ adb_prefix = f"adb -s {serial}" if serial else "adb"
148
+ package = projects[project_name].package
149
+ if package:
150
+ bash_check(f"{adb_prefix} uninstall {package}")
151
+ bash(f"{adb_prefix} install {apks[0]}")
152
+ permissions = projects[project_name].grant_permissions
153
+ if permissions and not no_grant_permissions:
154
+ if not package:
155
+ raise typer.BadParameter(
156
+ f"{project_name} has grant_permissions but no 'package' field in unity-build.json"
157
+ )
158
+ for permission in permissions:
159
+ print(f"Granting {permission} to {package}")
160
+ bash(f"{adb_prefix} shell pm grant {package} {permission}")
161
+ print("Done.")
162
+ else:
163
+ if not executables:
164
+ print("No linux executable found in install source")
165
+ raise SystemExit(1)
166
+ executable = executables[0]
167
+ os.chmod(executable, executable.stat().st_mode | 0o755)
168
+ print(f"Launching: {executable.name}")
169
+ bash_handoff(str(executable))
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from bashrun import bash
7
+ from pydantic_settings import BaseSettings
8
+
9
+ from .cache import restore, save
10
+ from .license_restore import license_cache_tag
11
+ from .setup import configure_git
12
+ from .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
+ def activate(oras_push: bool) -> None:
28
+ configure_git(settings.github_workspace)
29
+ install_oras()
30
+
31
+ license_directory = Path.home() / ".local" / "share" / "unity3d" / "Unity"
32
+ license_directory.mkdir(parents=True, exist_ok=True)
33
+
34
+ tag = license_cache_tag()
35
+ cache_hit = False
36
+ if oras_push:
37
+ cache_hit = restore(settings.cache_registry, "unity-license", tag, license_directory)
38
+
39
+ if not cache_hit:
40
+ bash(
41
+ f'unity-editor -batchmode -nographics -quit -serial "{settings.unity_serial}"'
42
+ f' -username "{settings.unity_email}" -password "{settings.unity_password}" -logFile /dev/stdout'
43
+ )
44
+
45
+ if oras_push and not cache_hit:
46
+ save(settings.cache_registry, "unity-license", tag, license_directory, ["Unity_lic.ulf"])
47
+
48
+
49
+ @activate_app.command()
50
+ def activate_main(oras_push: bool = typer.Option(False, help="Push activated ULF to ORAS cache")) -> None:
51
+ activate(oras_push)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from pydantic_settings import BaseSettings
9
+
10
+ from .cache import restore
11
+
12
+
13
+ # LICENSE_CACHE_TAG env pins the tag for a whole CI run; without it, each call
14
+ # re-reads "now UTC" and a run straddling midnight save/restore-misses itself.
15
+ def license_cache_tag() -> str:
16
+ override = os.environ.get("LICENSE_CACHE_TAG")
17
+ if override:
18
+ return override
19
+ return f"v-{datetime.now(UTC).strftime('%Y-%m-%d')}"
20
+
21
+
22
+ class Settings(BaseSettings):
23
+ cache_registry: str
24
+
25
+
26
+ tag_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
27
+
28
+
29
+ @tag_app.command()
30
+ def tag_main() -> None:
31
+ print(license_cache_tag())
32
+
33
+
34
+ def restore_license() -> None:
35
+ settings = Settings.model_validate({})
36
+ license_directory = Path.home() / ".local" / "share" / "unity3d" / "Unity"
37
+ license_directory.mkdir(parents=True, exist_ok=True)
38
+ restore(settings.cache_registry, "unity-license", license_cache_tag(), license_directory, required=True)
@@ -0,0 +1,28 @@
1
+ from typing import Annotated
2
+
3
+ import typer
4
+
5
+ from .projects import load_unity_projects
6
+ from .unity import prepare_unity_project, run_unity_batchmode
7
+
8
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
9
+
10
+
11
+ @app.command()
12
+ def lock_unity(project: Annotated[str | None, typer.Option(help="Limit to a specific project.")] = None) -> None:
13
+ config = load_unity_projects()
14
+
15
+ if project is not None and project not in config:
16
+ raise typer.BadParameter(f"Unknown project '{project}'. Valid: {', '.join(config)}")
17
+
18
+ all_projects = {name: project_config.path for name, project_config in config.items()}
19
+ projects = {project: all_projects[project]} if project else all_projects
20
+
21
+ for name, project_path in projects.items():
22
+ lock_file = project_path / "Packages" / "packages-lock.json"
23
+ print(f"Resolving {name} ({lock_file})...")
24
+
25
+ prepare_unity_project(project_path)
26
+ run_unity_batchmode(project_path, strict_exit=False)
27
+
28
+ print(" Done")
unity_devkit/matrix.py ADDED
@@ -0,0 +1,31 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from .projects import load_unity_projects
5
+ from .unity import LICENSE_MODULE, PLATFORM_CONFIGS, UNITYCI_IMAGE_REVISION, read_editor_version
6
+
7
+
8
+ def main() -> None:
9
+ projects = load_unity_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
+ module = PLATFORM_CONFIGS[platform]["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
+ "module": module,
26
+ "editor-image": f"unityci/editor:{version}-{module}-{UNITYCI_IMAGE_REVISION}",
27
+ })
28
+
29
+ license_version = max(editor_versions)
30
+ print(f"matrix={json.dumps({'include': matrix})}")
31
+ print(f"license-image=unityci/editor:{license_version}-{LICENSE_MODULE}-{UNITYCI_IMAGE_REVISION}")