unity-devkit 0.1.17__tar.gz → 0.1.18__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.17 → unity_devkit-0.1.18}/PKG-INFO +1 -1
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/pyproject.toml +1 -1
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/build_unity.py +1 -1
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/ci_build_unity.py +1 -1
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/compile_check_unity.py +1 -1
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/dispatch_workflow_generator.py +4 -4
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/install.py +2 -2
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/lock_unity.py +2 -2
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/matrix.py +4 -4
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/player_build.py +4 -4
- unity_devkit-0.1.18/src/unity_devkit/projects.py +73 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/test_unity.py +3 -3
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/tests/test_build_player.py +3 -3
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/tests/test_matrix.py +8 -5
- unity_devkit-0.1.18/tests/test_projects.py +120 -0
- unity_devkit-0.1.17/src/unity_devkit/projects.py +0 -55
- unity_devkit-0.1.17/tests/test_projects.py +0 -88
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/.gitignore +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/__init__.py +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/license.py +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/license_restore.py +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/src/unity_devkit/py.typed +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/tests/fixtures/environment-dump.json +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/tests/test_dispatch_workflow_generator.py +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/tests/test_prepare.py +0 -0
- {unity_devkit-0.1.17 → unity_devkit-0.1.18}/tests/test_run_unity_batchmode.py +0 -0
|
@@ -9,7 +9,7 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
9
9
|
|
|
10
10
|
@app.command()
|
|
11
11
|
def build_unity(
|
|
12
|
-
project: Annotated[str, typer.Option(help="Unity project name (
|
|
12
|
+
project: Annotated[str, typer.Option(help="Unity project name (the 'name' field of its unity-devkit.json)")],
|
|
13
13
|
build: Annotated[str, typer.Option(help="Build target from the project's builds list (e.g. AndroidMobile)")],
|
|
14
14
|
version: Annotated[
|
|
15
15
|
str,
|
|
@@ -63,7 +63,7 @@ def ci_build_unity(
|
|
|
63
63
|
unity_project_path = resolve_unity_project(project).path
|
|
64
64
|
if unity_project_path.resolve() != project_path.resolve():
|
|
65
65
|
raise SystemExit(
|
|
66
|
-
f"--project-path {project_path} does not match
|
|
66
|
+
f"--project-path {project_path} does not match discovered project '{project}' at {unity_project_path}"
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
with ci_step(f"Build {project} [{platform}]"):
|
|
@@ -11,7 +11,7 @@ COMPILE_ERROR_SIGNATURES = ("error CS",)
|
|
|
11
11
|
|
|
12
12
|
@app.command()
|
|
13
13
|
def compile_check_unity(
|
|
14
|
-
project: Annotated[str, typer.Option(help="Unity project name (
|
|
14
|
+
project: Annotated[str, typer.Option(help="Unity project name (the 'name' field of its unity-devkit.json)")],
|
|
15
15
|
execute_method: Annotated[
|
|
16
16
|
str | None,
|
|
17
17
|
typer.Option(help="Static method to run after load (Class.Method) — one editor session per invocation"),
|
|
@@ -7,7 +7,7 @@ import typer
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict
|
|
8
8
|
|
|
9
9
|
from .compile_check_unity import COMPILE_ERROR_SIGNATURES
|
|
10
|
-
from .projects import
|
|
10
|
+
from .projects import discover_projects
|
|
11
11
|
from .player_build import prepare_unity_project, run_unity_batchmode
|
|
12
12
|
|
|
13
13
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
@@ -58,7 +58,7 @@ class EnvironmentDump(BaseModel):
|
|
|
58
58
|
|
|
59
59
|
@app.command()
|
|
60
60
|
def unity_dispatch(
|
|
61
|
-
project: Annotated[str, typer.Option(help="Unity project name (
|
|
61
|
+
project: Annotated[str, typer.Option(help="Unity project name (the 'name' field of its unity-devkit.json)")],
|
|
62
62
|
output: Annotated[Path, typer.Option(help="Workflow file to write (or check with --check)")],
|
|
63
63
|
build_workflow: Annotated[
|
|
64
64
|
str,
|
|
@@ -69,13 +69,13 @@ def unity_dispatch(
|
|
|
69
69
|
],
|
|
70
70
|
environment_config_class: Annotated[
|
|
71
71
|
str | None,
|
|
72
|
-
typer.Option(help="Env class file path override (defaults to
|
|
72
|
+
typer.Option(help="Env class file path override (defaults to unity-devkit.json's environment_config)"),
|
|
73
73
|
] = None,
|
|
74
74
|
check: Annotated[
|
|
75
75
|
bool, typer.Option("--check", help="Compare against the existing file instead of writing; fail on drift")
|
|
76
76
|
] = False,
|
|
77
77
|
) -> None:
|
|
78
|
-
projects =
|
|
78
|
+
projects = discover_projects()
|
|
79
79
|
if project not in projects:
|
|
80
80
|
raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
|
|
81
81
|
|
|
@@ -8,7 +8,7 @@ from typing import Annotated, Any
|
|
|
8
8
|
import typer
|
|
9
9
|
from bashrun.bash import bash, bash_handoff, bash_output
|
|
10
10
|
|
|
11
|
-
from .projects import
|
|
11
|
+
from .projects import discover_projects
|
|
12
12
|
from .player_build import build_player
|
|
13
13
|
|
|
14
14
|
INSTALLABLE_TARGETS = {"AndroidMobile", "MagicLeap2", "Linux"}
|
|
@@ -43,7 +43,7 @@ def main(
|
|
|
43
43
|
),
|
|
44
44
|
] = False,
|
|
45
45
|
) -> None:
|
|
46
|
-
projects =
|
|
46
|
+
projects = discover_projects()
|
|
47
47
|
|
|
48
48
|
project_name = next((name for name in projects if name.lower() == project.lower()), None)
|
|
49
49
|
if project_name is None:
|
|
@@ -2,7 +2,7 @@ from typing import Annotated
|
|
|
2
2
|
|
|
3
3
|
import typer
|
|
4
4
|
|
|
5
|
-
from .projects import
|
|
5
|
+
from .projects import discover_projects
|
|
6
6
|
from .player_build import prepare_unity_project, run_unity_batchmode
|
|
7
7
|
|
|
8
8
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
@@ -10,7 +10,7 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
10
10
|
|
|
11
11
|
@app.command()
|
|
12
12
|
def lock_unity(project: Annotated[str | None, typer.Option(help="Limit to a specific project.")] = None) -> None:
|
|
13
|
-
config =
|
|
13
|
+
config = discover_projects()
|
|
14
14
|
|
|
15
15
|
if project is not None and project not in config:
|
|
16
16
|
raise typer.BadParameter(f"Unknown project '{project}'. Valid: {', '.join(config)}")
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from .projects import
|
|
4
|
+
from .projects import discover_projects
|
|
5
5
|
from .player_build import LICENSE_IMAGE_MODULE, PLATFORM_CONFIGS, UNITYCI_IMAGE_REVISION, read_editor_version
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def build_matrix() -> None:
|
|
9
|
-
projects =
|
|
9
|
+
projects = discover_projects()
|
|
10
10
|
matrix: list[dict[str, str]] = []
|
|
11
11
|
editor_versions: set[str] = set()
|
|
12
12
|
|
|
@@ -28,7 +28,7 @@ def build_matrix() -> None:
|
|
|
28
28
|
|
|
29
29
|
if not editor_versions:
|
|
30
30
|
raise SystemExit(
|
|
31
|
-
"No projects with builds declared — build-unity-matrix needs at least one unity-devkit.json
|
|
31
|
+
"No projects with builds declared — build-unity-matrix needs at least one unity-devkit.json with a non-empty 'platforms' map"
|
|
32
32
|
)
|
|
33
33
|
license_version = max(editor_versions)
|
|
34
34
|
print(f"matrix={json.dumps({'include': matrix})}")
|
|
@@ -36,7 +36,7 @@ def build_matrix() -> None:
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def compile_check_matrix() -> None:
|
|
39
|
-
projects =
|
|
39
|
+
projects = discover_projects()
|
|
40
40
|
matrix: list[dict[str, str]] = []
|
|
41
41
|
editor_versions: set[str] = set()
|
|
42
42
|
|
|
@@ -9,7 +9,7 @@ from typing import TypedDict
|
|
|
9
9
|
|
|
10
10
|
from bashrun.bash import CalledProcessError, bash, bash_pipe
|
|
11
11
|
|
|
12
|
-
from .projects import
|
|
12
|
+
from .projects import ProjectEntry, discover_projects
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class PlatformConfig(TypedDict):
|
|
@@ -135,8 +135,8 @@ def editor_version(project_path: Path) -> str | None:
|
|
|
135
135
|
return None
|
|
136
136
|
|
|
137
137
|
|
|
138
|
-
def resolve_unity_project(project: str) ->
|
|
139
|
-
projects =
|
|
138
|
+
def resolve_unity_project(project: str) -> ProjectEntry:
|
|
139
|
+
projects = discover_projects()
|
|
140
140
|
if project not in projects:
|
|
141
141
|
raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
|
|
142
142
|
return projects[project]
|
|
@@ -182,7 +182,7 @@ def build_player(
|
|
|
182
182
|
valid_builds = project_config.builds or []
|
|
183
183
|
if not valid_builds:
|
|
184
184
|
raise SystemExit(
|
|
185
|
-
f"Project '{project}' declares no builds — add
|
|
185
|
+
f"Project '{project}' declares no builds — add platform keys to its unity-devkit.json 'platforms' map"
|
|
186
186
|
)
|
|
187
187
|
if build not in valid_builds:
|
|
188
188
|
raise SystemExit(f"Unknown build '{build}' for project '{project}'. Valid: {', '.join(valid_builds)}")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
7
|
+
|
|
8
|
+
BUILD_CONFIG_FILENAME = "unity-devkit.json"
|
|
9
|
+
PROJECT_MARKER = Path("ProjectSettings") / "ProjectVersion.txt"
|
|
10
|
+
PRUNE_DIRECTORIES = {".git", "Library", "Temp", "obj", "Build", "node_modules", "__pycache__"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BuildConfigFile(BaseModel):
|
|
14
|
+
model_config = ConfigDict(extra="ignore")
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
platforms: dict[str, Any] | None = None
|
|
18
|
+
|
|
19
|
+
@field_validator("name")
|
|
20
|
+
@classmethod
|
|
21
|
+
def nonempty_name(cls, value: str) -> str:
|
|
22
|
+
if not value.strip():
|
|
23
|
+
raise ValueError("unity-devkit.json 'name' must be a non-empty string")
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProjectEntry(BaseModel):
|
|
28
|
+
path: Path
|
|
29
|
+
builds: list[str] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def discover_projects() -> dict[str, ProjectEntry]:
|
|
33
|
+
root = Path.cwd()
|
|
34
|
+
discovered: dict[str, ProjectEntry] = {}
|
|
35
|
+
first_seen: dict[str, Path] = {}
|
|
36
|
+
|
|
37
|
+
for directory in visible_directories(root):
|
|
38
|
+
config_path = directory / BUILD_CONFIG_FILENAME
|
|
39
|
+
if not config_path.is_file():
|
|
40
|
+
continue
|
|
41
|
+
if not (directory / PROJECT_MARKER).is_file():
|
|
42
|
+
raise SystemExit(
|
|
43
|
+
f"{config_path} sits in {directory}, which is not a Unity project: missing {PROJECT_MARKER}"
|
|
44
|
+
)
|
|
45
|
+
parsed = BuildConfigFile.model_validate_json(config_path.read_text(encoding="utf-8"))
|
|
46
|
+
if parsed.name in discovered:
|
|
47
|
+
raise SystemExit(
|
|
48
|
+
f"Duplicate project name '{parsed.name}' — declared at {config_path} "
|
|
49
|
+
f"and previously at {first_seen[parsed.name]}"
|
|
50
|
+
)
|
|
51
|
+
first_seen[parsed.name] = config_path
|
|
52
|
+
builds = list(parsed.platforms.keys()) if parsed.platforms else None
|
|
53
|
+
discovered[parsed.name] = ProjectEntry(path=directory, builds=builds)
|
|
54
|
+
|
|
55
|
+
if not discovered:
|
|
56
|
+
raise SystemExit(
|
|
57
|
+
f"No {BUILD_CONFIG_FILENAME} found under {root} — declare Unity projects with a "
|
|
58
|
+
f"{BUILD_CONFIG_FILENAME} per project root (carrying a 'name' field; 'platforms' "
|
|
59
|
+
f"keys are the build targets)"
|
|
60
|
+
)
|
|
61
|
+
return discovered
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def visible_directories(root: Path) -> Iterator[Path]:
|
|
65
|
+
for directory, subdirectories, _filenames in os.walk(root):
|
|
66
|
+
subdirectories[:] = sorted(
|
|
67
|
+
name for name in subdirectories if name not in PRUNE_DIRECTORIES and not name.startswith(".")
|
|
68
|
+
)
|
|
69
|
+
yield Path(directory)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def directories_containing(root: Path, filename: str) -> list[Path]:
|
|
73
|
+
return [directory for directory in visible_directories(root) if (directory / filename).is_file()]
|
|
@@ -5,7 +5,7 @@ from typing import Annotated
|
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
|
-
from .projects import
|
|
8
|
+
from .projects import discover_projects
|
|
9
9
|
from .player_build import run_unity_batchmode
|
|
10
10
|
|
|
11
11
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
@@ -13,13 +13,13 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
13
13
|
|
|
14
14
|
@app.command()
|
|
15
15
|
def main(
|
|
16
|
-
project: Annotated[str, typer.Option(help="Unity project name (
|
|
16
|
+
project: Annotated[str, typer.Option(help="Unity project name (the 'name' field of its unity-devkit.json)")],
|
|
17
17
|
test_platform: Annotated[str, typer.Option(help="Unity test platform (EditMode or PlayMode)")] = "EditMode",
|
|
18
18
|
results: Annotated[Path, typer.Option(help="Output path for NUnit XML results")] = Path(
|
|
19
19
|
"artifacts/unity-test-results.xml"
|
|
20
20
|
),
|
|
21
21
|
) -> None:
|
|
22
|
-
projects =
|
|
22
|
+
projects = discover_projects()
|
|
23
23
|
if project not in projects:
|
|
24
24
|
raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
|
|
25
25
|
|
|
@@ -21,10 +21,10 @@ def write_repository(tmp_path: Path, project_settings: str, *, builds: list[str]
|
|
|
21
21
|
(project / "ProjectSettings").mkdir(parents=True)
|
|
22
22
|
(project / "ProjectSettings" / "ProjectSettings.asset").write_text(project_settings)
|
|
23
23
|
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
24
|
-
|
|
24
|
+
config: dict[str, object] = {"name": "tool"}
|
|
25
25
|
if builds is not None:
|
|
26
|
-
|
|
27
|
-
(
|
|
26
|
+
config["platforms"] = {platform: {} for platform in builds}
|
|
27
|
+
(project / "unity-devkit.json").write_text(json.dumps(config))
|
|
28
28
|
return project
|
|
29
29
|
|
|
30
30
|
|
|
@@ -6,17 +6,20 @@ import pytest
|
|
|
6
6
|
from unity_devkit import matrix
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def write_repository(tmp_path: Path,
|
|
9
|
+
def write_repository(tmp_path: Path, builds: list[str] | None = None) -> None:
|
|
10
10
|
project = tmp_path / "apps" / "Tool"
|
|
11
11
|
(project / "ProjectSettings").mkdir(parents=True)
|
|
12
12
|
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
13
|
-
|
|
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))
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def test_matrix_emits_entries_from_catalog(
|
|
17
20
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
18
21
|
) -> None:
|
|
19
|
-
write_repository(tmp_path,
|
|
22
|
+
write_repository(tmp_path, builds=["Linux"])
|
|
20
23
|
monkeypatch.chdir(tmp_path)
|
|
21
24
|
|
|
22
25
|
matrix.build_matrix()
|
|
@@ -37,7 +40,7 @@ def test_matrix_emits_entries_from_catalog(
|
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
def test_matrix_fails_without_builds(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
40
|
-
write_repository(tmp_path
|
|
43
|
+
write_repository(tmp_path)
|
|
41
44
|
monkeypatch.chdir(tmp_path)
|
|
42
45
|
|
|
43
46
|
with pytest.raises(SystemExit, match="No projects with builds declared"):
|
|
@@ -47,7 +50,7 @@ def test_matrix_fails_without_builds(tmp_path: Path, monkeypatch: pytest.MonkeyP
|
|
|
47
50
|
def test_check_matrix_includes_path_only_projects(
|
|
48
51
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
49
52
|
) -> None:
|
|
50
|
-
write_repository(tmp_path
|
|
53
|
+
write_repository(tmp_path)
|
|
51
54
|
monkeypatch.chdir(tmp_path)
|
|
52
55
|
|
|
53
56
|
matrix.compile_check_matrix()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from unity_devkit.projects import directories_containing, discover_projects
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_unity_project(root: Path, relative: str) -> Path:
|
|
11
|
+
project = root / relative
|
|
12
|
+
(project / "ProjectSettings").mkdir(parents=True)
|
|
13
|
+
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
14
|
+
return project
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def write_build_config(project_dir: Path, config: dict[str, object]) -> Path:
|
|
18
|
+
config_path = project_dir / "unity-devkit.json"
|
|
19
|
+
config_path.write_text(json.dumps(config))
|
|
20
|
+
return config_path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_discovers_project_with_platforms_keys_as_builds(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
24
|
+
project = create_unity_project(tmp_path, "Alpha")
|
|
25
|
+
write_build_config(project, {"name": "Alpha", "platforms": {"Linux": {}, "AndroidMobile": {}}})
|
|
26
|
+
monkeypatch.chdir(tmp_path)
|
|
27
|
+
|
|
28
|
+
projects = discover_projects()
|
|
29
|
+
|
|
30
|
+
assert set(projects) == {"Alpha"}
|
|
31
|
+
assert projects["Alpha"].path == project
|
|
32
|
+
assert projects["Alpha"].builds == ["Linux", "AndroidMobile"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_name_is_decoupled_from_directory_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
36
|
+
project = create_unity_project(tmp_path, "apps/capture-tool")
|
|
37
|
+
write_build_config(project, {"name": "capture"})
|
|
38
|
+
monkeypatch.chdir(tmp_path)
|
|
39
|
+
|
|
40
|
+
projects = discover_projects()
|
|
41
|
+
|
|
42
|
+
assert set(projects) == {"capture"}
|
|
43
|
+
assert projects["capture"].path == project
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_path_only_project_has_no_builds(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
47
|
+
project = create_unity_project(tmp_path, "Harness")
|
|
48
|
+
write_build_config(project, {"name": "Harness"})
|
|
49
|
+
monkeypatch.chdir(tmp_path)
|
|
50
|
+
|
|
51
|
+
projects = discover_projects()
|
|
52
|
+
|
|
53
|
+
assert set(projects) == {"Harness"}
|
|
54
|
+
assert projects["Harness"].builds is None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_no_build_config_found_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
58
|
+
monkeypatch.chdir(tmp_path)
|
|
59
|
+
|
|
60
|
+
with pytest.raises(SystemExit, match=r"No unity-devkit\.json found"):
|
|
61
|
+
discover_projects()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_config_outside_unity_project_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
65
|
+
not_a_project = tmp_path / "not-a-project"
|
|
66
|
+
not_a_project.mkdir()
|
|
67
|
+
write_build_config(not_a_project, {"name": "broken"})
|
|
68
|
+
monkeypatch.chdir(tmp_path)
|
|
69
|
+
|
|
70
|
+
with pytest.raises(SystemExit, match="not a Unity project"):
|
|
71
|
+
discover_projects()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_duplicate_project_name_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
75
|
+
first = create_unity_project(tmp_path, "apps/one")
|
|
76
|
+
write_build_config(first, {"name": "dup"})
|
|
77
|
+
second = create_unity_project(tmp_path, "apps/two")
|
|
78
|
+
write_build_config(second, {"name": "dup"})
|
|
79
|
+
monkeypatch.chdir(tmp_path)
|
|
80
|
+
|
|
81
|
+
with pytest.raises(SystemExit, match="Duplicate project name 'dup'"):
|
|
82
|
+
discover_projects()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_ignores_csharp_owned_fields(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
86
|
+
project = create_unity_project(tmp_path, "App")
|
|
87
|
+
write_build_config(
|
|
88
|
+
project,
|
|
89
|
+
{
|
|
90
|
+
"name": "App",
|
|
91
|
+
"environment_config": "Assets/App/UnityEnv.cs",
|
|
92
|
+
"platforms": {"AndroidMobile": {"render_pipeline": "Assets/Settings/Android.asset"}},
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
monkeypatch.chdir(tmp_path)
|
|
96
|
+
|
|
97
|
+
projects = discover_projects()
|
|
98
|
+
|
|
99
|
+
assert projects["App"].builds == ["AndroidMobile"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_missing_name_field_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
103
|
+
project = create_unity_project(tmp_path, "Alpha")
|
|
104
|
+
write_build_config(project, {"platforms": {"Linux": {}}})
|
|
105
|
+
monkeypatch.chdir(tmp_path)
|
|
106
|
+
|
|
107
|
+
with pytest.raises(ValidationError):
|
|
108
|
+
discover_projects()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_directories_containing_honors_prune_list(tmp_path: Path) -> None:
|
|
112
|
+
vendored = tmp_path / "vendor" / "Library" / "SomeProject"
|
|
113
|
+
(vendored / "ProjectSettings").mkdir(parents=True)
|
|
114
|
+
(vendored / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
115
|
+
real = create_unity_project(tmp_path, "Real")
|
|
116
|
+
|
|
117
|
+
settings_directories = directories_containing(tmp_path, "ProjectVersion.txt")
|
|
118
|
+
|
|
119
|
+
assert real / "ProjectSettings" in settings_directories
|
|
120
|
+
assert vendored / "ProjectSettings" not in settings_directories
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from collections.abc import Iterator
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, ConfigDict, TypeAdapter, field_validator
|
|
6
|
-
|
|
7
|
-
CATALOG_FILENAME = "unity-devkit.json"
|
|
8
|
-
PROJECT_MARKER = Path("ProjectSettings") / "ProjectVersion.txt"
|
|
9
|
-
PRUNE_DIRECTORIES = {".git", "Library", "Temp", "obj", "Build", "node_modules", "__pycache__"}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class CatalogEntry(BaseModel):
|
|
13
|
-
model_config = ConfigDict(extra="forbid")
|
|
14
|
-
|
|
15
|
-
path: Path
|
|
16
|
-
builds: list[str] | None = None
|
|
17
|
-
|
|
18
|
-
@field_validator("path")
|
|
19
|
-
@classmethod
|
|
20
|
-
def anchor_relative_path(cls, value: Path) -> Path:
|
|
21
|
-
return value if value.is_absolute() else Path.cwd() / value
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
CATALOG_ADAPTER = TypeAdapter(dict[str, CatalogEntry])
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def load_catalog() -> dict[str, CatalogEntry]:
|
|
28
|
-
catalog_path = Path.cwd() / CATALOG_FILENAME
|
|
29
|
-
if not catalog_path.is_file():
|
|
30
|
-
raise SystemExit(
|
|
31
|
-
f"No {CATALOG_FILENAME} catalog at {catalog_path} — declare Unity projects as catalog entries at the repo root"
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
catalog = CATALOG_ADAPTER.validate_json(catalog_path.read_text(encoding="utf-8"))
|
|
35
|
-
if not catalog:
|
|
36
|
-
raise SystemExit(f"{catalog_path} declares no projects — at least one catalog entry is required")
|
|
37
|
-
|
|
38
|
-
for name, entry in catalog.items():
|
|
39
|
-
if not (entry.path / PROJECT_MARKER).is_file():
|
|
40
|
-
raise SystemExit(
|
|
41
|
-
f"Catalog entry '{name}' points at {entry.path}, which is not a Unity project: missing {PROJECT_MARKER}"
|
|
42
|
-
)
|
|
43
|
-
return catalog
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def visible_directories(root: Path) -> Iterator[Path]:
|
|
47
|
-
for directory, subdirectories, _filenames in os.walk(root):
|
|
48
|
-
subdirectories[:] = sorted(
|
|
49
|
-
name for name in subdirectories if name not in PRUNE_DIRECTORIES and not name.startswith(".")
|
|
50
|
-
)
|
|
51
|
-
yield Path(directory)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def directories_containing(root: Path, filename: str) -> list[Path]:
|
|
55
|
-
return [directory for directory in visible_directories(root) if (directory / filename).is_file()]
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from pydantic import ValidationError
|
|
6
|
-
|
|
7
|
-
from unity_devkit.projects import directories_containing, load_catalog
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def write_catalog(root: Path, catalog: dict[str, object]) -> Path:
|
|
11
|
-
catalog_path = root / "unity-devkit.json"
|
|
12
|
-
catalog_path.write_text(json.dumps(catalog))
|
|
13
|
-
return catalog_path
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def create_unity_project(root: Path, relative: str) -> Path:
|
|
17
|
-
project = root / relative
|
|
18
|
-
(project / "ProjectSettings").mkdir(parents=True)
|
|
19
|
-
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
20
|
-
return project
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_catalog_entry_loads_with_intent_fields(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
24
|
-
create_unity_project(tmp_path, "Alpha")
|
|
25
|
-
write_catalog(tmp_path, {"Alpha": {"path": "Alpha", "builds": ["Linux"]}})
|
|
26
|
-
monkeypatch.chdir(tmp_path)
|
|
27
|
-
|
|
28
|
-
projects = load_catalog()
|
|
29
|
-
|
|
30
|
-
assert set(projects) == {"Alpha"}
|
|
31
|
-
assert projects["Alpha"].path == Path.cwd() / "Alpha"
|
|
32
|
-
assert projects["Alpha"].builds == ["Linux"]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_catalog_name_is_decoupled_from_directory_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
36
|
-
create_unity_project(tmp_path, "apps/capture-tool")
|
|
37
|
-
write_catalog(tmp_path, {"capture": {"path": "apps/capture-tool"}})
|
|
38
|
-
monkeypatch.chdir(tmp_path)
|
|
39
|
-
|
|
40
|
-
projects = load_catalog()
|
|
41
|
-
|
|
42
|
-
assert set(projects) == {"capture"}
|
|
43
|
-
assert projects["capture"].path == Path.cwd() / "apps" / "capture-tool"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_missing_catalog_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
47
|
-
monkeypatch.chdir(tmp_path)
|
|
48
|
-
|
|
49
|
-
with pytest.raises(SystemExit, match=r"No unity-devkit\.json catalog"):
|
|
50
|
-
load_catalog()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_empty_catalog_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
54
|
-
write_catalog(tmp_path, {})
|
|
55
|
-
monkeypatch.chdir(tmp_path)
|
|
56
|
-
|
|
57
|
-
with pytest.raises(SystemExit, match="declares no projects"):
|
|
58
|
-
load_catalog()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_entry_outside_unity_project_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
62
|
-
(tmp_path / "not-a-project").mkdir()
|
|
63
|
-
write_catalog(tmp_path, {"broken": {"path": "not-a-project"}})
|
|
64
|
-
monkeypatch.chdir(tmp_path)
|
|
65
|
-
|
|
66
|
-
with pytest.raises(SystemExit, match="not a Unity project"):
|
|
67
|
-
load_catalog()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_unknown_entry_key_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
71
|
-
create_unity_project(tmp_path, "Alpha")
|
|
72
|
-
write_catalog(tmp_path, {"Alpha": {"path": "Alpha", "unity-build": True}})
|
|
73
|
-
monkeypatch.chdir(tmp_path)
|
|
74
|
-
|
|
75
|
-
with pytest.raises(ValidationError):
|
|
76
|
-
load_catalog()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_directories_containing_honors_prune_list(tmp_path: Path) -> None:
|
|
80
|
-
vendored = tmp_path / "vendor" / "Library" / "SomeProject"
|
|
81
|
-
(vendored / "ProjectSettings").mkdir(parents=True)
|
|
82
|
-
(vendored / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
83
|
-
real = create_unity_project(tmp_path, "Real")
|
|
84
|
-
|
|
85
|
-
settings_directories = directories_containing(tmp_path, "ProjectVersion.txt")
|
|
86
|
-
|
|
87
|
-
assert real / "ProjectSettings" in settings_directories
|
|
88
|
-
assert vendored / "ProjectSettings" not in settings_directories
|
|
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
|