unity-devkit 0.1.18__tar.gz → 0.1.19__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 (24) hide show
  1. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/PKG-INFO +1 -1
  2. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/pyproject.toml +1 -1
  3. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/ci_build_unity.py +26 -1
  4. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/install.py +85 -33
  5. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/.gitignore +0 -0
  6. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/__init__.py +0 -0
  7. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/build_unity.py +0 -0
  8. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/compile_check_unity.py +0 -0
  9. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/dispatch_workflow_generator.py +0 -0
  10. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/license.py +0 -0
  11. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/license_restore.py +0 -0
  12. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/lock_unity.py +0 -0
  13. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/matrix.py +0 -0
  14. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/player_build.py +0 -0
  15. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/projects.py +0 -0
  16. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/py.typed +0 -0
  17. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/src/unity_devkit/test_unity.py +0 -0
  18. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/tests/fixtures/environment-dump.json +0 -0
  19. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/tests/test_build_player.py +0 -0
  20. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/tests/test_dispatch_workflow_generator.py +0 -0
  21. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/tests/test_matrix.py +0 -0
  22. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/tests/test_prepare.py +0 -0
  23. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/tests/test_projects.py +0 -0
  24. {unity_devkit-0.1.18 → unity_devkit-0.1.19}/tests/test_run_unity_batchmode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unity-devkit
3
- Version: 0.1.18
3
+ Version: 0.1.19
4
4
  Summary: Unity build, license, and CI tooling
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: bashrun>=0.1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unity-devkit"
3
- version = "0.1.18"
3
+ version = "0.1.19"
4
4
  description = "Unity build, license, and CI tooling"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from typing import Annotated
6
6
 
7
7
  import typer
8
+ from bashrun.bash import bash
8
9
  from pydantic_settings import BaseSettings
9
10
 
10
11
  from ci_devkit.cache import restore, save
@@ -28,7 +29,8 @@ def ci_build_unity(
28
29
  project_path: Annotated[Path, typer.Option(help="Path to Unity project")],
29
30
  platform: Annotated[str, typer.Option(help="Target platform")],
30
31
  cache_key: Annotated[str, typer.Option(help="Cache key prefix")],
31
- registry: Annotated[str, typer.Option(help="OCI registry path")],
32
+ registry: Annotated[str, typer.Option(help="OCI registry path for the library cache")],
33
+ builds_registry: Annotated[str, typer.Option(help="OCI registry path for build artifacts")],
32
34
  run_number: Annotated[int, typer.Option(help="CI run number")] = 0,
33
35
  branch: Annotated[str, typer.Option(help="Git branch name")] = "dev",
34
36
  environment_preset: Annotated[
@@ -96,3 +98,26 @@ def ci_build_unity(
96
98
  for file in build_directory.rglob("*"):
97
99
  if file.suffix in {".apk", ".exe"}:
98
100
  shutil.copy2(file, artifact_directory / file.name)
101
+
102
+ with ci_step("Push build artifacts"):
103
+ artifact_directory = Path("/tmp/unity-builds")
104
+ if not artifact_directory.is_dir() or not any(artifact_directory.iterdir()):
105
+ print("No build artifacts to push")
106
+ else:
107
+ reference_base = f"{builds_registry}/{project}-{platform}".lower()
108
+ tags = (branch_slug, f"run-{run_number}")
109
+ if platform == "Linux":
110
+ staging = Path("/tmp/unity-builds-push")
111
+ staging.mkdir(parents=True, exist_ok=True)
112
+ tar_path = staging / "build.tar"
113
+ bash(f"tar -cf {tar_path} -C {artifact_directory} .")
114
+ try:
115
+ for build_tag in tags:
116
+ bash(f"oras push {reference_base}:{build_tag} build.tar", cwd=staging)
117
+ finally:
118
+ tar_path.unlink(missing_ok=True)
119
+ else:
120
+ joined = " ".join(sorted(p.name for p in artifact_directory.iterdir() if p.is_file()))
121
+ for build_tag in tags:
122
+ bash(f"oras push {reference_base}:{build_tag} {joined}", cwd=artifact_directory)
123
+ print(f"Pushed build artifacts: {reference_base} ({', '.join(tags)})")
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  import os
4
+ import shutil
5
5
  from pathlib import Path
6
- from typing import Annotated, Any
6
+ from typing import Annotated
7
7
 
8
8
  import typer
9
- from bashrun.bash import bash, bash_handoff, bash_output
9
+ from bashrun.bash import bash, bash_check, bash_handoff, bash_output
10
10
 
11
11
  from .projects import discover_projects
12
12
  from .player_build import build_player
@@ -27,9 +27,12 @@ def main(
27
27
  ] = None,
28
28
  branch: Annotated[
29
29
  str | None,
30
- typer.Option("--branch", "-b", help="Branch to find latest successful run (default: current git branch)"),
30
+ typer.Option("--branch", "-b", help="Branch to pull the build from (default: current git branch)"),
31
+ ] = None,
32
+ run: Annotated[
33
+ int | None,
34
+ typer.Option("--run", "-r", help="Specific CI run number (github.run_number) to pull"),
31
35
  ] = None,
32
- run: Annotated[int | None, typer.Option("--run", "-r", help="Specific GitHub Actions run ID")] = None,
33
36
  serial: Annotated[str | None, typer.Option("--serial", "-s", help="adb device serial")] = None,
34
37
  build_locally: Annotated[
35
38
  bool,
@@ -38,10 +41,18 @@ def main(
38
41
  "-B",
39
42
  help=(
40
43
  "Build the project locally via `build-unity` and install the produced APK / "
41
- "linux executable. Skips the GitHub Actions artifact lookup; --branch / --run are ignored."
44
+ "linux executable. Skips the OCI pull; --branch / --run are ignored."
42
45
  ),
43
46
  ),
44
47
  ] = False,
48
+ list_tags: Annotated[
49
+ bool,
50
+ typer.Option("--list", help="List available build tags for (project, target) and exit"),
51
+ ] = False,
52
+ dry_run: Annotated[
53
+ bool,
54
+ typer.Option("--dry-run", help="Print what would be fetched/installed without doing it"),
55
+ ] = False,
45
56
  ) -> None:
46
57
  projects = discover_projects()
47
58
 
@@ -68,51 +79,73 @@ def main(
68
79
  if serial and target_name not in ADB_TARGETS:
69
80
  print(f"Warning: --serial is ignored for target '{target_name}'")
70
81
 
82
+ package_name = f"{project_name}-{target_name}".lower()
83
+
84
+ if list_tags:
85
+ _require_oras()
86
+ reference_base = _resolve_reference_base(package_name)
87
+ _login_ghcr()
88
+ if bash_check(f"oras repo tags {reference_base}"):
89
+ output = bash_output(f"oras repo tags {reference_base}")
90
+ if output.strip():
91
+ print(f"Available tags for {reference_base}:")
92
+ print(output, end="")
93
+ else:
94
+ print(f"No tags found for {reference_base}")
95
+ else:
96
+ print(f"No tags found for {reference_base}")
97
+ return
98
+
71
99
  if build_locally:
72
100
  if branch or run:
73
101
  print("Warning: --branch / --run are ignored when --build is set")
102
+ if dry_run:
103
+ print(f"Would build {project_name} [{target_name}] locally")
104
+ if target_name in ADB_TARGETS:
105
+ serial_part = f" -s {serial}" if serial else ""
106
+ print(f"Would install: adb{serial_part} install <apk>")
107
+ elif target_name == "Linux":
108
+ print("Would launch the produced executable")
109
+ return
74
110
  produced = build_player(project_name, target_name)
75
111
  apks = [path for path in produced if path.suffix == ".apk"]
76
112
  executables = [path for path in produced if path.suffix in {".exe", ".x86_64"}]
77
113
  else:
78
- artifact_name = f"{project_name}-{target_name}"
79
-
80
114
  resolved_branch = branch
81
115
  if not resolved_branch:
82
116
  resolved_branch = bash_output("git rev-parse --abbrev-ref HEAD").strip()
83
117
  if resolved_branch == "HEAD":
84
118
  raise typer.BadParameter("HEAD is detached; pass --branch explicitly")
85
119
 
86
- if run:
87
- run_id = str(run)
88
- else:
89
- owner_repo = bash_output("gh repo view --json nameWithOwner --jq .nameWithOwner").strip()
90
- output = bash_output(
91
- f"gh api repos/{owner_repo}/actions/artifacts --method GET -f name={artifact_name}"
92
- f" -f per_page=10 --jq .artifacts"
93
- )
94
- artifacts: list[dict[str, Any]] = json.loads(output)
95
- run_id = next(
96
- (
97
- str(artifact["workflow_run"]["id"])
98
- for artifact in artifacts
99
- if artifact["workflow_run"]["head_branch"] == resolved_branch
100
- ),
101
- None,
102
- )
103
- if run_id is None:
104
- print(f"No artifact '{artifact_name}' found on branch '{resolved_branch}'")
105
- raise SystemExit(1)
120
+ tag = f"run-{run}" if run else resolved_branch.replace("/", "-")
106
121
 
107
- print(f"Run: {run_id}")
108
- print(f"Artifact: {artifact_name}")
122
+ reference_base = _resolve_reference_base(package_name)
123
+ reference = f"{reference_base}:{tag}"
124
+ cache_path = CACHE_ROOT / tag / package_name
125
+
126
+ if dry_run:
127
+ print(f"Would pull: {reference}")
128
+ print(f"Cache: {cache_path}")
129
+ if target_name in ADB_TARGETS:
130
+ serial_part = f" -s {serial}" if serial else ""
131
+ print(f"Would install: adb{serial_part} install <apk>")
132
+ elif target_name == "Linux":
133
+ print("Would extract build.tar and launch the executable")
134
+ return
135
+
136
+ _require_oras()
137
+ _login_ghcr()
109
138
 
110
- cache_path = CACHE_ROOT / run_id / artifact_name
111
139
  if cache_path.is_dir() and any(cache_path.iterdir()):
112
- print(f"Using cached artifact: {cache_path}")
140
+ print(f"Using cached build: {cache_path}")
113
141
  else:
114
142
  cache_path.mkdir(parents=True, exist_ok=True)
115
- bash(f"gh run download {run_id} --name {artifact_name} --dir {cache_path}")
143
+ bash(f"oras pull {reference} -o {cache_path}")
144
+ if target_name == "Linux":
145
+ tar_path = cache_path / "build.tar"
146
+ if tar_path.exists():
147
+ bash(f"tar -xf {tar_path} -C {cache_path}")
148
+ tar_path.unlink()
116
149
 
117
150
  apks = sorted(cache_path.rglob("*.apk"))
118
151
 
@@ -148,3 +181,22 @@ def main(
148
181
  os.chmod(executable, executable.stat().st_mode | 0o755)
149
182
  print(f"Launching: {executable.name}")
150
183
  bash_handoff(str(executable))
184
+
185
+
186
+ def _require_oras() -> None:
187
+ if not shutil.which("oras"):
188
+ print("Error: oras is not on PATH. Install it:")
189
+ print(" macOS: brew install oras")
190
+ print(" Linux: https://oras.land/docs/install")
191
+ print(" Windows: scoop install oras")
192
+ raise SystemExit(1)
193
+
194
+
195
+ def _resolve_reference_base(package_name: str) -> str:
196
+ owner_repo = bash_output("gh repo view --json nameWithOwner --jq .nameWithOwner").strip()
197
+ return f"ghcr.io/{owner_repo}/builds/{package_name}".lower()
198
+
199
+
200
+ def _login_ghcr() -> None:
201
+ token = bash_output("gh auth token").strip()
202
+ bash("oras login ghcr.io --username oauth2 --password-stdin", stdin_text=token)
File without changes