unity-devkit 0.1.18__tar.gz → 0.1.20__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.18 → unity_devkit-0.1.20}/PKG-INFO +1 -1
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/pyproject.toml +1 -1
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/ci_build_unity.py +26 -1
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/dispatch_workflow_generator.py +14 -4
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/install.py +85 -33
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/tests/test_dispatch_workflow_generator.py +21 -6
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/.gitignore +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/__init__.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/build_unity.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/compile_check_unity.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/license.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/license_restore.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/lock_unity.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/matrix.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/player_build.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/projects.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/py.typed +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/src/unity_devkit/test_unity.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/tests/fixtures/environment-dump.json +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/tests/test_build_player.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/tests/test_matrix.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/tests/test_prepare.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/tests/test_projects.py +0 -0
- {unity_devkit-0.1.18 → unity_devkit-0.1.20}/tests/test_run_unity_batchmode.py +0 -0
|
@@ -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)})")
|
|
@@ -63,8 +63,8 @@ def unity_dispatch(
|
|
|
63
63
|
build_workflow: Annotated[
|
|
64
64
|
str,
|
|
65
65
|
typer.Option(
|
|
66
|
-
help="The build job's uses: value — the consumer's pinned build-unity.yml reference "
|
|
67
|
-
"(owner/repo/.github/workflows/build-unity.yml@sha, or ./.github/workflows/build-unity.yml inside unity-devkit)"
|
|
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
68
|
),
|
|
69
69
|
],
|
|
70
70
|
environment_config_class: Annotated[
|
|
@@ -160,6 +160,11 @@ def render_dispatch_workflow(dump: EnvironmentDump, *, project: str, output: str
|
|
|
160
160
|
f"{dump.class_name} —"
|
|
161
161
|
)
|
|
162
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
|
+
|
|
163
168
|
lines = [
|
|
164
169
|
header,
|
|
165
170
|
"# edit the class, then regenerate this workflow.",
|
|
@@ -169,13 +174,18 @@ def render_dispatch_workflow(dump: EnvironmentDump, *, project: str, output: str
|
|
|
169
174
|
" workflow_dispatch:",
|
|
170
175
|
" inputs:",
|
|
171
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),
|
|
172
180
|
"",
|
|
173
181
|
"jobs:",
|
|
174
182
|
" build:",
|
|
175
183
|
f" uses: {build_workflow}",
|
|
176
184
|
" with:",
|
|
177
|
-
"
|
|
178
|
-
"
|
|
185
|
+
" runner-labels: ${{ inputs.runner-labels || '\"ubuntu-latest\"' }}",
|
|
186
|
+
" version: ${{ inputs.version || '' }}",
|
|
187
|
+
" environment-preset: ${{ inputs['environment-preset'] || '' }}",
|
|
188
|
+
" development: ${{ inputs.development || false }}",
|
|
179
189
|
]
|
|
180
190
|
if transport_lines:
|
|
181
191
|
lines.append(" environment-fields: |-")
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
140
|
+
print(f"Using cached build: {cache_path}")
|
|
113
141
|
else:
|
|
114
142
|
cache_path.mkdir(parents=True, exist_ok=True)
|
|
115
|
-
bash(f"
|
|
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)
|
|
@@ -13,9 +13,7 @@ from unity_devkit.dispatch_workflow_generator import (
|
|
|
13
13
|
from unity_devkit.player_build import parse_environment_fields
|
|
14
14
|
|
|
15
15
|
FIXTURE_DUMP = Path(__file__).parent / "fixtures" / "environment-dump.json"
|
|
16
|
-
PINNED_BUILD_WORKFLOW =
|
|
17
|
-
"outernet-foundation/unity-devkit/.github/workflows/build-unity.yml@cfd487e0e19b3d98046a2680a210137ec0d32832"
|
|
18
|
-
)
|
|
16
|
+
PINNED_BUILD_WORKFLOW = "outernet-foundation/unity-devkit/.github/workflows/build-unity-internal.yml@cfd487e0e19b3d98046a2680a210137ec0d32832"
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
def load_fixture_dump() -> EnvironmentDump:
|
|
@@ -53,7 +51,7 @@ def test_render_states_the_override_law_once_on_the_preset_input() -> None:
|
|
|
53
51
|
assert "environment field" not in workflow
|
|
54
52
|
assert "empty keeps the preset value" not in workflow
|
|
55
53
|
assert "checked overrides the preset value" not in workflow
|
|
56
|
-
assert workflow.count("description:") ==
|
|
54
|
+
assert workflow.count("description:") == 5
|
|
57
55
|
|
|
58
56
|
|
|
59
57
|
def test_render_maps_field_types_to_dispatch_inputs_without_descriptions() -> None:
|
|
@@ -159,11 +157,28 @@ def test_rendered_workflow_is_valid_yaml() -> None:
|
|
|
159
157
|
"apiUrl",
|
|
160
158
|
"portNumber",
|
|
161
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"] == ""
|
|
162
166
|
assert document["jobs"]["build"]["uses"] == PINNED_BUILD_WORKFLOW
|
|
163
167
|
assert document["jobs"]["build"]["secrets"] == "inherit"
|
|
164
168
|
with_block = document["jobs"]["build"]["with"]
|
|
165
|
-
assert with_block["
|
|
166
|
-
assert with_block["
|
|
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
|
|
167
182
|
|
|
168
183
|
|
|
169
184
|
def test_render_header_embeds_the_full_regen_command() -> None:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|