unity-devkit 0.1.8__py3-none-any.whl → 0.1.10__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.
- unity_devkit/compile_unity.py +9 -9
- unity_devkit/install.py +75 -72
- unity_devkit/license.py +2 -6
- unity_devkit/license_restore.py +10 -10
- unity_devkit/unity.py +77 -86
- {unity_devkit-0.1.8.dist-info → unity_devkit-0.1.10.dist-info}/METADATA +1 -1
- unity_devkit-0.1.10.dist-info/RECORD +18 -0
- unity_devkit-0.1.8.dist-info/RECORD +0 -18
- {unity_devkit-0.1.8.dist-info → unity_devkit-0.1.10.dist-info}/WHEEL +0 -0
- {unity_devkit-0.1.8.dist-info → unity_devkit-0.1.10.dist-info}/entry_points.txt +0 -0
- {unity_devkit-0.1.8.dist-info → unity_devkit-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {unity_devkit-0.1.8.dist-info → unity_devkit-0.1.10.dist-info}/licenses/NOTICE +0 -0
unity_devkit/compile_unity.py
CHANGED
|
@@ -8,6 +8,15 @@ from .unity import prepare_unity_project, resolve_unity_build, run_unity_batchmo
|
|
|
8
8
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
@app.command()
|
|
12
|
+
def compile_unity(
|
|
13
|
+
project: Annotated[str, typer.Option(help="Unity project name (directory containing unity-build.json)")],
|
|
14
|
+
build: Annotated[str, typer.Option(help="Build target from the project's builds list (e.g. android-mobile)")],
|
|
15
|
+
) -> None:
|
|
16
|
+
for artifact in build_unity_project(project, build):
|
|
17
|
+
print(f"Built: {artifact}")
|
|
18
|
+
|
|
19
|
+
|
|
11
20
|
def build_unity_project(project: str, build: str) -> list[Path]:
|
|
12
21
|
project_config, build_flag, execute_method = resolve_unity_build(project, build)
|
|
13
22
|
project_path = project_config.path
|
|
@@ -29,15 +38,6 @@ def build_unity_project(project: str, build: str) -> list[Path]:
|
|
|
29
38
|
return produced
|
|
30
39
|
|
|
31
40
|
|
|
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
41
|
def snapshot_artifacts(build_directory: Path) -> dict[Path, int]:
|
|
42
42
|
if not build_directory.is_dir():
|
|
43
43
|
return {}
|
unity_devkit/install.py
CHANGED
|
@@ -9,7 +9,7 @@ import typer
|
|
|
9
9
|
from bashrun.bash import bash, bash_check, bash_handoff, bash_output
|
|
10
10
|
|
|
11
11
|
from .compile_unity import build_unity_project
|
|
12
|
-
from .projects import
|
|
12
|
+
from .projects import load_unity_projects
|
|
13
13
|
|
|
14
14
|
INSTALLABLE_TARGETS = {"android-mobile", "magicleap", "linux64"}
|
|
15
15
|
ADB_TARGETS = {"android-mobile", "magicleap"}
|
|
@@ -18,68 +18,6 @@ CACHE_ROOT = Path.home() / ".unity-devkit" / "builds"
|
|
|
18
18
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
19
19
|
|
|
20
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
21
|
@app.command()
|
|
84
22
|
def main(
|
|
85
23
|
project: Annotated[str, typer.Option("--project", "-p", help="Unity project name")],
|
|
@@ -116,8 +54,26 @@ def main(
|
|
|
116
54
|
] = False,
|
|
117
55
|
) -> None:
|
|
118
56
|
projects = load_unity_projects()
|
|
119
|
-
|
|
120
|
-
|
|
57
|
+
|
|
58
|
+
project_name = next((name for name in projects if name.lower() == project.lower()), None)
|
|
59
|
+
if project_name is None:
|
|
60
|
+
valid = ", ".join(projects.keys())
|
|
61
|
+
raise typer.BadParameter(f"Unknown project '{project}'. Valid projects: {valid}")
|
|
62
|
+
|
|
63
|
+
project_config = projects[project_name]
|
|
64
|
+
installable = [build for build in (project_config.builds or []) if build in INSTALLABLE_TARGETS]
|
|
65
|
+
if target is None:
|
|
66
|
+
if len(installable) != 1:
|
|
67
|
+
valid = ", ".join(installable) if installable else "(none)"
|
|
68
|
+
raise typer.BadParameter(
|
|
69
|
+
f"--target is required for {project_name} (multiple installable targets). Valid targets: {valid}"
|
|
70
|
+
)
|
|
71
|
+
target_name = installable[0]
|
|
72
|
+
else:
|
|
73
|
+
target_name = next((build for build in installable if build.lower() == target.lower()), None)
|
|
74
|
+
if target_name is None:
|
|
75
|
+
valid = ", ".join(installable)
|
|
76
|
+
raise typer.BadParameter(f"No installable target '{target}' for {project_name}. Valid targets: {valid}")
|
|
121
77
|
|
|
122
78
|
if serial and target_name not in ADB_TARGETS:
|
|
123
79
|
print(f"Warning: --serial is ignored for target '{target_name}'")
|
|
@@ -130,13 +86,60 @@ def main(
|
|
|
130
86
|
executables = [path for path in produced if path.suffix == ".exe"]
|
|
131
87
|
else:
|
|
132
88
|
artifact_name = f"{project_name}-{target_name}"
|
|
133
|
-
|
|
134
|
-
|
|
89
|
+
|
|
90
|
+
resolved_branch = branch
|
|
91
|
+
if not resolved_branch:
|
|
92
|
+
resolved_branch = bash_output("git rev-parse --abbrev-ref HEAD").strip()
|
|
93
|
+
if resolved_branch == "HEAD":
|
|
94
|
+
raise typer.BadParameter("HEAD is detached; pass --branch explicitly")
|
|
95
|
+
|
|
96
|
+
if run:
|
|
97
|
+
run_id = str(run)
|
|
98
|
+
else:
|
|
99
|
+
owner_repo = bash_output("gh repo view --json nameWithOwner --jq .nameWithOwner").strip()
|
|
100
|
+
output = bash_output(
|
|
101
|
+
f"gh api repos/{owner_repo}/actions/artifacts --method GET -f name={artifact_name}"
|
|
102
|
+
f" -f per_page=10 --jq .artifacts"
|
|
103
|
+
)
|
|
104
|
+
artifacts: list[dict[str, Any]] = json.loads(output)
|
|
105
|
+
run_id = next(
|
|
106
|
+
(
|
|
107
|
+
str(artifact["workflow_run"]["id"])
|
|
108
|
+
for artifact in artifacts
|
|
109
|
+
if artifact["workflow_run"]["head_branch"] == resolved_branch
|
|
110
|
+
),
|
|
111
|
+
None,
|
|
112
|
+
)
|
|
113
|
+
if run_id is None:
|
|
114
|
+
print(f"No artifact '{artifact_name}' found on branch '{resolved_branch}'")
|
|
115
|
+
raise SystemExit(1)
|
|
116
|
+
|
|
135
117
|
print(f"Run: {run_id}")
|
|
136
118
|
print(f"Artifact: {artifact_name}")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
119
|
+
|
|
120
|
+
cache_path = CACHE_ROOT / run_id / artifact_name
|
|
121
|
+
if cache_path.is_dir() and any(cache_path.iterdir()):
|
|
122
|
+
print(f"Using cached artifact: {cache_path}")
|
|
123
|
+
else:
|
|
124
|
+
cache_path.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
bash(f"gh run download {run_id} --name {artifact_name} --dir {cache_path}")
|
|
126
|
+
|
|
127
|
+
apks = sorted(cache_path.rglob("*.apk"))
|
|
128
|
+
|
|
129
|
+
executables = []
|
|
130
|
+
if target_name == "linux64":
|
|
131
|
+
executable = next(
|
|
132
|
+
(
|
|
133
|
+
item
|
|
134
|
+
for item in cache_path.iterdir()
|
|
135
|
+
if item.is_file() and (cache_path / f"{item.stem}_Data").is_dir()
|
|
136
|
+
),
|
|
137
|
+
None,
|
|
138
|
+
)
|
|
139
|
+
if executable is None:
|
|
140
|
+
print("No linux64 executable found in artifact (expected a file with a matching _Data/ directory)")
|
|
141
|
+
raise SystemExit(1)
|
|
142
|
+
executables = [executable]
|
|
140
143
|
|
|
141
144
|
if target_name in ADB_TARGETS:
|
|
142
145
|
if not apks:
|
|
@@ -145,11 +148,11 @@ def main(
|
|
|
145
148
|
|
|
146
149
|
print(f"Installing: {apks[0].name}")
|
|
147
150
|
adb_prefix = f"adb -s {serial}" if serial else "adb"
|
|
148
|
-
package =
|
|
151
|
+
package = project_config.package
|
|
149
152
|
if package:
|
|
150
153
|
bash_check(f"{adb_prefix} uninstall {package}")
|
|
151
154
|
bash(f"{adb_prefix} install {apks[0]}")
|
|
152
|
-
permissions =
|
|
155
|
+
permissions = project_config.grant_permissions
|
|
153
156
|
if permissions and not no_grant_permissions:
|
|
154
157
|
if not package:
|
|
155
158
|
raise typer.BadParameter(
|
unity_devkit/license.py
CHANGED
|
@@ -24,7 +24,8 @@ settings = Settings.model_validate({})
|
|
|
24
24
|
activate_app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
@activate_app.command()
|
|
28
|
+
def activate_main(oras_push: bool = typer.Option(False, help="Push activated ULF to ORAS cache")) -> None:
|
|
28
29
|
configure_git(settings.github_workspace)
|
|
29
30
|
install_oras()
|
|
30
31
|
|
|
@@ -44,8 +45,3 @@ def activate(oras_push: bool) -> None:
|
|
|
44
45
|
|
|
45
46
|
if oras_push and not cache_hit:
|
|
46
47
|
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)
|
unity_devkit/license_restore.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
from datetime import UTC, datetime
|
|
4
|
+
import os
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
import typer
|
|
@@ -10,15 +10,6 @@ from pydantic_settings import BaseSettings
|
|
|
10
10
|
from ci_devkit.cache import restore
|
|
11
11
|
|
|
12
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
13
|
class Settings(BaseSettings):
|
|
23
14
|
cache_registry: str
|
|
24
15
|
|
|
@@ -36,3 +27,12 @@ def restore_license() -> None:
|
|
|
36
27
|
license_directory = Path.home() / ".local" / "share" / "unity3d" / "Unity"
|
|
37
28
|
license_directory.mkdir(parents=True, exist_ok=True)
|
|
38
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')}"
|
unity_devkit/unity.py
CHANGED
|
@@ -32,82 +32,6 @@ QUIET_FAILURE_SIGNATURES = ("An error occurred while resolving packages:",)
|
|
|
32
32
|
QUIET_FAILURE_BLOCK_LINE_LIMIT = 20
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def find_unity_editor(project_path: Path) -> str:
|
|
36
|
-
try:
|
|
37
|
-
editor = find_editor_for_version(read_editor_version(project_path))
|
|
38
|
-
except ValueError as error:
|
|
39
|
-
raise SystemExit(str(error)) from error
|
|
40
|
-
return str(editor)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def find_editor_for_version(version: str) -> Path:
|
|
44
|
-
path_editor = shutil.which("unity-editor")
|
|
45
|
-
if path_editor:
|
|
46
|
-
return Path(path_editor)
|
|
47
|
-
|
|
48
|
-
if sys.platform == "win32":
|
|
49
|
-
candidates = [Path(f"C:/Program Files/Unity/Hub/Editor/{version}/Editor/Unity.exe")]
|
|
50
|
-
else:
|
|
51
|
-
candidates = [
|
|
52
|
-
Path(f"/opt/unity/{version}/Editor/Unity"),
|
|
53
|
-
Path.home() / f"Unity/Hub/Editor/{version}/Editor/Unity",
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
for candidate in candidates:
|
|
57
|
-
if candidate.is_file():
|
|
58
|
-
return candidate
|
|
59
|
-
|
|
60
|
-
searched = ", ".join(str(candidate) for candidate in candidates)
|
|
61
|
-
raise ValueError(f"Cannot find Unity {version} editor. Searched: {searched}")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def read_editor_version(project_path: Path) -> str:
|
|
65
|
-
version_file = project_path / "ProjectSettings" / "ProjectVersion.txt"
|
|
66
|
-
version = editor_version(project_path)
|
|
67
|
-
if version is not None:
|
|
68
|
-
return version
|
|
69
|
-
if not version_file.exists():
|
|
70
|
-
raise SystemExit(f"Cannot find {version_file} — is this a Unity project?")
|
|
71
|
-
raise SystemExit(f"Cannot parse editor version from {version_file}")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def editor_version(project_path: Path) -> str | None:
|
|
75
|
-
version_file = project_path / "ProjectSettings" / "ProjectVersion.txt"
|
|
76
|
-
if not version_file.exists():
|
|
77
|
-
return None
|
|
78
|
-
for line in version_file.read_text().splitlines():
|
|
79
|
-
if line.startswith("m_EditorVersion:"):
|
|
80
|
-
return line.split(":", 1)[1].strip()
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def prepare_unity_project(project_path: Path) -> None:
|
|
85
|
-
stale_lockfile = project_path / "Temp" / "UnityLockfile"
|
|
86
|
-
if stale_lockfile.exists():
|
|
87
|
-
stale_lockfile.unlink()
|
|
88
|
-
|
|
89
|
-
bash("dotnet tool restore")
|
|
90
|
-
bash(f"dotnet nugetforunity restore {project_path}")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def unity_batchmode_command(project_path: Path, nographics: bool = True, *, auto_quit: bool = True) -> str:
|
|
94
|
-
editor = find_unity_editor(project_path)
|
|
95
|
-
# Player builds need a real GfxDevice: Unity 6 compresses Android textures (ASTC/ETC2) on the
|
|
96
|
-
# GPU, and under -nographics the Null device falls back to a path that produces corrupt textures.
|
|
97
|
-
# xvfb-run (added below) supplies the display the dropped -nographics would otherwise stand in for.
|
|
98
|
-
graphics_flag = " -nographics" if nographics else ""
|
|
99
|
-
quit_flag = " -quit" if auto_quit else ""
|
|
100
|
-
command = f"{editor} -batchmode{graphics_flag}{quit_flag} -projectPath {project_path.resolve()}"
|
|
101
|
-
if sys.platform != "win32":
|
|
102
|
-
if shutil.which("xvfb-run"):
|
|
103
|
-
command = f"xvfb-run {command}"
|
|
104
|
-
# Unity runs `adb kill-server` on Android build teardown; strip the
|
|
105
|
-
# env var so the kill lands on a local daemon, not whatever
|
|
106
|
-
# ADB_SERVER_SOCKET points at.
|
|
107
|
-
command = f"env -u ADB_SERVER_SOCKET {command}"
|
|
108
|
-
return command
|
|
109
|
-
|
|
110
|
-
|
|
111
35
|
def run_unity_batchmode(
|
|
112
36
|
project_path: Path,
|
|
113
37
|
extra_flags: str = "",
|
|
@@ -129,17 +53,9 @@ def run_unity_batchmode(
|
|
|
129
53
|
bash(command, log_path=log_path)
|
|
130
54
|
except CalledProcessError as error:
|
|
131
55
|
returncode = error.returncode
|
|
132
|
-
failure_block = quiet_failure_block(log_path)
|
|
133
|
-
if failure_block is not None:
|
|
134
|
-
raise SystemExit(f"Unity reported a package-manager failure (exit code {returncode}):\n{failure_block}")
|
|
135
|
-
if returncode != 0 and strict_exit:
|
|
136
|
-
raise SystemExit(f"Unity exited {returncode}; full editor log at {log_path}")
|
|
137
|
-
if returncode != 0:
|
|
138
|
-
print(f" WARNING: Unity exited {returncode}; no package-manager failure found — full editor log at {log_path}")
|
|
139
|
-
|
|
140
56
|
|
|
141
|
-
def quiet_failure_block(log_path: Path) -> str | None:
|
|
142
57
|
lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
58
|
+
failure_block = None
|
|
143
59
|
for index, line in enumerate(lines):
|
|
144
60
|
if not any(signature in line for signature in QUIET_FAILURE_SIGNATURES):
|
|
145
61
|
continue
|
|
@@ -148,7 +64,73 @@ def quiet_failure_block(log_path: Path) -> str | None:
|
|
|
148
64
|
if block and not candidate.strip():
|
|
149
65
|
break
|
|
150
66
|
block.append(candidate)
|
|
151
|
-
|
|
67
|
+
failure_block = "\n".join(block)
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
if failure_block is not None:
|
|
71
|
+
raise SystemExit(f"Unity reported a package-manager failure (exit code {returncode}):\n{failure_block}")
|
|
72
|
+
if returncode != 0 and strict_exit:
|
|
73
|
+
raise SystemExit(f"Unity exited {returncode}; full editor log at {log_path}")
|
|
74
|
+
if returncode != 0:
|
|
75
|
+
print(f" WARNING: Unity exited {returncode}; no package-manager failure found — full editor log at {log_path}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def unity_batchmode_command(project_path: Path, nographics: bool = True, *, auto_quit: bool = True) -> str:
|
|
79
|
+
editor = str(find_editor_for_version(read_editor_version(project_path)))
|
|
80
|
+
# Player builds need a real GfxDevice: Unity 6 compresses Android textures (ASTC/ETC2) on the
|
|
81
|
+
# GPU, and under -nographics the Null device falls back to a path that produces corrupt textures.
|
|
82
|
+
# xvfb-run (added below) supplies the display the dropped -nographics would otherwise stand in for.
|
|
83
|
+
graphics_flag = " -nographics" if nographics else ""
|
|
84
|
+
quit_flag = " -quit" if auto_quit else ""
|
|
85
|
+
command = f"{editor} -batchmode{graphics_flag}{quit_flag} -projectPath {project_path.resolve()}"
|
|
86
|
+
if sys.platform != "win32":
|
|
87
|
+
if shutil.which("xvfb-run"):
|
|
88
|
+
command = f"xvfb-run {command}"
|
|
89
|
+
# Unity runs `adb kill-server` on Android build teardown; strip the
|
|
90
|
+
# env var so the kill lands on a local daemon, not whatever
|
|
91
|
+
# ADB_SERVER_SOCKET points at.
|
|
92
|
+
command = f"env -u ADB_SERVER_SOCKET {command}"
|
|
93
|
+
return command
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def read_editor_version(project_path: Path) -> str:
|
|
97
|
+
version_file = project_path / "ProjectSettings" / "ProjectVersion.txt"
|
|
98
|
+
version = editor_version(project_path)
|
|
99
|
+
if version is not None:
|
|
100
|
+
return version
|
|
101
|
+
if not version_file.exists():
|
|
102
|
+
raise SystemExit(f"Cannot find {version_file} — is this a Unity project?")
|
|
103
|
+
raise SystemExit(f"Cannot parse editor version from {version_file}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_editor_for_version(version: str) -> Path:
|
|
107
|
+
path_editor = shutil.which("unity-editor")
|
|
108
|
+
if path_editor:
|
|
109
|
+
return Path(path_editor)
|
|
110
|
+
|
|
111
|
+
if sys.platform == "win32":
|
|
112
|
+
candidates = [Path(f"C:/Program Files/Unity/Hub/Editor/{version}/Editor/Unity.exe")]
|
|
113
|
+
else:
|
|
114
|
+
candidates = [
|
|
115
|
+
Path(f"/opt/unity/{version}/Editor/Unity"),
|
|
116
|
+
Path.home() / f"Unity/Hub/Editor/{version}/Editor/Unity",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
for candidate in candidates:
|
|
120
|
+
if candidate.is_file():
|
|
121
|
+
return candidate
|
|
122
|
+
|
|
123
|
+
searched = ", ".join(str(candidate) for candidate in candidates)
|
|
124
|
+
raise ValueError(f"Cannot find Unity {version} editor. Searched: {searched}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def editor_version(project_path: Path) -> str | None:
|
|
128
|
+
version_file = project_path / "ProjectSettings" / "ProjectVersion.txt"
|
|
129
|
+
if not version_file.exists():
|
|
130
|
+
return None
|
|
131
|
+
for line in version_file.read_text().splitlines():
|
|
132
|
+
if line.startswith("m_EditorVersion:"):
|
|
133
|
+
return line.split(":", 1)[1].strip()
|
|
152
134
|
return None
|
|
153
135
|
|
|
154
136
|
|
|
@@ -171,3 +153,12 @@ def resolve_unity_build(project: str, build: str) -> tuple[UnityProject, str, st
|
|
|
171
153
|
raise SystemExit(f"No platform config for build '{build}'. Valid: {', '.join(PLATFORM_CONFIGS)}")
|
|
172
154
|
|
|
173
155
|
return project_config, PLATFORM_CONFIGS[build]["build_flag"], execute_method
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def prepare_unity_project(project_path: Path) -> None:
|
|
159
|
+
stale_lockfile = project_path / "Temp" / "UnityLockfile"
|
|
160
|
+
if stale_lockfile.exists():
|
|
161
|
+
stale_lockfile.unlink()
|
|
162
|
+
|
|
163
|
+
bash("dotnet tool restore")
|
|
164
|
+
bash(f"dotnet nugetforunity restore {project_path}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
unity_devkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
unity_devkit/build_unity.py,sha256=2DWbaI8UROzGS_iKa-mXjD7jzNxPNyQvHSa6F_2t-c0,4197
|
|
3
|
+
unity_devkit/compile_unity.py,sha256=bSXLwuvMmgT9Uc5l8UTjVJvgYPiEpwFaOFL6iyems8k,1789
|
|
4
|
+
unity_devkit/install.py,sha256=k0AXeFt-B1yj-Aq5vgvcol9X1KnNa3x74tQNjV1-XSc,6961
|
|
5
|
+
unity_devkit/license.py,sha256=iiMdos8kMk4p5uYYroIWUzxvusXtcMRdPkIDXsYTC9c,1484
|
|
6
|
+
unity_devkit/license_restore.py,sha256=Tbt2Kt1axszqj4H6K5Fx9_nqRL-dpL5XTVnxnmP4PZw,1077
|
|
7
|
+
unity_devkit/lock_unity.py,sha256=9UNMTj5ekFqqyXpRZ4fSru6SeyTda23jqtJ48rb0HOs,1009
|
|
8
|
+
unity_devkit/matrix.py,sha256=wb8hkEx6f8wnoEjSZszM9lpPofh5a5LspCTtsKqKKQ0,1158
|
|
9
|
+
unity_devkit/projects.py,sha256=pWk6klQJ5_cdT8gcwQTkB1dK7y7UuNTk5NHYUCLvBuI,1941
|
|
10
|
+
unity_devkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
unity_devkit/test_unity.py,sha256=Zdr-e2Qx_OgC1huAT_ZgL_YgKnu4FjCRJEhTW7pXMyQ,1068
|
|
12
|
+
unity_devkit/unity.py,sha256=tTxpP4Dq_ThiL4Z6nEyeLdPMI0V8IBLVG_o1tkq1hjE,6499
|
|
13
|
+
unity_devkit-0.1.10.dist-info/METADATA,sha256=xb_RGAsMYheurCJOJ3p6-UXhuALSRKkeLFL9TfYdeZc,352
|
|
14
|
+
unity_devkit-0.1.10.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
15
|
+
unity_devkit-0.1.10.dist-info/entry_points.txt,sha256=RKjFLiz02NSodLcgVDp8pnYX6zEa7VhrH_1GXnnt9d8,381
|
|
16
|
+
unity_devkit-0.1.10.dist-info/licenses/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
|
|
17
|
+
unity_devkit-0.1.10.dist-info/licenses/NOTICE,sha256=jvglZza3GCdN7v4_TLuXUqX9C_uABlwAKUkcIeKwk_8,26
|
|
18
|
+
unity_devkit-0.1.10.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
unity_devkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
unity_devkit/build_unity.py,sha256=2DWbaI8UROzGS_iKa-mXjD7jzNxPNyQvHSa6F_2t-c0,4197
|
|
3
|
-
unity_devkit/compile_unity.py,sha256=ktcZIdjUIODyJbXG4iHbRNmiIkY4bwF4jujnT-oIUBs,1789
|
|
4
|
-
unity_devkit/install.py,sha256=lafdb1_eEPowQbA8uTI9FZnv1WDOos6xnsN88gR--p4,6993
|
|
5
|
-
unity_devkit/license.py,sha256=ygcbS9O2579ZoJgcNEWt-QRInIM6ZEJBLnh3noKlfPM,1549
|
|
6
|
-
unity_devkit/license_restore.py,sha256=M-YTBcSLIw9WM2ksQI5_mSqCcoKD5oYpnlhVnRuK1Aw,1077
|
|
7
|
-
unity_devkit/lock_unity.py,sha256=9UNMTj5ekFqqyXpRZ4fSru6SeyTda23jqtJ48rb0HOs,1009
|
|
8
|
-
unity_devkit/matrix.py,sha256=wb8hkEx6f8wnoEjSZszM9lpPofh5a5LspCTtsKqKKQ0,1158
|
|
9
|
-
unity_devkit/projects.py,sha256=pWk6klQJ5_cdT8gcwQTkB1dK7y7UuNTk5NHYUCLvBuI,1941
|
|
10
|
-
unity_devkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
unity_devkit/test_unity.py,sha256=Zdr-e2Qx_OgC1huAT_ZgL_YgKnu4FjCRJEhTW7pXMyQ,1068
|
|
12
|
-
unity_devkit/unity.py,sha256=y13jfHENft29kmzlNLsqQm1cIFWn1R4zLTVkQgZLdqg,6780
|
|
13
|
-
unity_devkit-0.1.8.dist-info/METADATA,sha256=nRueGT3lVw7butiv21yQYc2zL5Fs886ZY6C_asdaMzs,351
|
|
14
|
-
unity_devkit-0.1.8.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
15
|
-
unity_devkit-0.1.8.dist-info/entry_points.txt,sha256=RKjFLiz02NSodLcgVDp8pnYX6zEa7VhrH_1GXnnt9d8,381
|
|
16
|
-
unity_devkit-0.1.8.dist-info/licenses/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
|
|
17
|
-
unity_devkit-0.1.8.dist-info/licenses/NOTICE,sha256=jvglZza3GCdN7v4_TLuXUqX9C_uABlwAKUkcIeKwk_8,26
|
|
18
|
-
unity_devkit-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|