unity-devkit 0.1.10__tar.gz → 0.1.12__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.10 → unity_devkit-0.1.12}/.github/workflows/release.yml +1 -1
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/AGENTS.md +3 -3
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/PKG-INFO +1 -1
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/README.md +4 -3
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/publish-config.json +1 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/pyproject.toml +7 -4
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/compile_unity.py +3 -1
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/matrix.py +4 -0
- unity_devkit-0.1.12/src/unity_devkit/projects.py +68 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/test_unity.py +3 -1
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/unity.py +14 -5
- unity_devkit-0.1.12/tests/test_prepare.py +36 -0
- unity_devkit-0.1.12/tests/test_projects.py +74 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/uv.lock +45 -0
- unity_devkit-0.1.10/src/unity_devkit/projects.py +0 -53
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/.gitattributes +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/.github/workflows/ci.yml +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/.github/workflows/unity-build.yml +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/.gitignore +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/CLAUDE.md +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/LICENSE +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/NOTICE +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/ruff.base.toml +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/ruff.toml +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/__init__.py +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/build_unity.py +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/install.py +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/license.py +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/license_restore.py +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/lock_unity.py +0 -0
- {unity_devkit-0.1.10 → unity_devkit-0.1.12}/src/unity_devkit/py.typed +0 -0
|
@@ -8,7 +8,7 @@ The repo and package renamed from `unity-buildkit` to `unity-devkit` (2026-09-21
|
|
|
8
8
|
|
|
9
9
|
## Release flow
|
|
10
10
|
|
|
11
|
-
Publishing rides `release.yml`, triggered by a successful CI run on a `main` push: the machinery — release-devkit's `publish-
|
|
11
|
+
Publishing rides `release.yml`, triggered by a successful CI run on a `main` push: the machinery — release-devkit's `publish-stable` (an inlined, version-pinned `uvx` step in the publish job, keeping OIDC identity local), never a project dependency (unity-devkit sits inside its own dependency graph; a project-level release-devkit edge is a resolver cycle) — computes the plan from the tag ledger and path-diff, patches the version ephemerally, and publishes to PyPI under OIDC trusted publishing (publisher bound to `release.yml`, no environment). The committed `pyproject.toml` version is permanently the `0.0.0.dev0` sentinel; the `unity-devkit-v*` tags are the version ledger (declared `major_minor` line in `publish-config.json`, patch-auto within the line). API-breaking changes ship with a manually bumped `major_minor` — patch-auto assumes additive changes.
|
|
12
12
|
|
|
13
13
|
## Shape
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ One flat module per concern under `src/unity_devkit/`:
|
|
|
16
16
|
|
|
17
17
|
| `uv run` command | Module | Notes |
|
|
18
18
|
|---|---|---|
|
|
19
|
-
| `compile-unity` | `compile_unity.py` | Local Unity build (APK or platform binary, suitable for `adb install`). Required flags: `--project <name>` and `--build <target>`,
|
|
19
|
+
| `compile-unity` | `compile_unity.py` | Local Unity build (APK or platform binary, suitable for `adb install`). Required flags: `--project <name>` and `--build <target>`, where the project's `unity-build.json` declares `builds` and `execute_methods`. Streams the editor log, prints output paths under `<project>/Build/`. |
|
|
20
20
|
| `install` | `install.py` | Install a Unity build onto an `adb`-connected device or launch a linux executable. Default: download the latest GitHub Actions artifact for `(project, target)` on the current branch (overridable with `--branch` / `--run`), cache it under `~/.unity-devkit/builds/{run_id}/`, `adb install` the APK (or `bash_handoff` the linux64 executable). With `--build` / `-B`, skips the artifact fetch and calls `compile-unity` locally instead, funneling the produced APK / executable through the same install path. Honours the manifest's `package` (for pre-install uninstall) and `grant_permissions` (post-install `adb shell pm grant`). |
|
|
21
21
|
| `lock-unity` | `lock_unity.py` | Lock Unity package versions for reproducible builds. |
|
|
22
22
|
| `test-unity` | `test_unity.py` | Run Unity editmode / playmode tests. |
|
|
@@ -33,7 +33,7 @@ Supporting modules: `projects.py` (manifest schema + discovery), `unity.py` (edi
|
|
|
33
33
|
|
|
34
34
|
**Every runtime dependency must be PyPI-resolvable.** This package publishes to and is consumed from PyPI, and a registry consumer resolves the whole graph transitively by name — any dependency that is not on PyPI breaks every consumer's install. [`bashrun`](https://github.com/outernet-foundation/bashrun) is on PyPI like the rest; git-source pins are a scratch-branch-only vehicle for testing unreleased changes.
|
|
35
35
|
|
|
36
|
-
**Unity project discovery is
|
|
36
|
+
**Unity project discovery is structural, with `unity-build.json` as an optional intent overlay.** The Unity commands discover projects by scanning the working-directory tree for `ProjectSettings/ProjectVersion.txt` (pruning `Library`, `Temp`, `obj`, `Build`, `node_modules`, `.git`, and dot-directories) — the marker Unity itself writes, so existence needs no opt-in file. The project name is the directory name. A `unity-build.json` manifest beside it carries only intent the project cannot otherwise express (`builds`, `execute_methods`, `package`, `grant_permissions`, `tag_prefix` — schema in `projects.py`); it is required by the verbs that consume those fields (`compile-unity`, `build-unity`, `install`, `unity-matrix` — which fail with pointers to the missing manifest fields) and ignored by the path-only verbs (`lock-unity`, `test-unity`). A manifest found in a directory without the structural marker is a hard error — a misconfigured manifest must fail loud, not silently skip. There is no central catalog: a project's build identity lives inside the project, so it travels with the project if it moves between repositories. Discovery anchors at the current working directory — run commands from the repo root. Vendored or nested projects that should not be touched are kept out of all-projects verbs (`lock-unity`, `test-unity` without `--project`) by the prune list or by passing `--project` explicitly.
|
|
37
37
|
|
|
38
38
|
**`unity-matrix` prints `$GITHUB_OUTPUT`-format lines, not bare JSON.** Its stdout is two `key=value` lines — `matrix=<JSON with an include array>` and `license-image=<unityci/editor tag>` — so the workflow step is exactly `uv run --no-sync unity-matrix >> "$GITHUB_OUTPUT"` with no shell logic. Each matrix entry carries an `editor-image` tag composed from the project's `ProjectSettings/ProjectVersion.txt` editor version, the platform's unityci module, and the `UNITYCI_IMAGE_REVISION` constant in `unity.py`; `license-image` uses the highest discovered editor version with the `linux-il2cpp` module. Nothing else pins an editor version: upgrading a project's editor in `ProjectVersion.txt` automatically switches its CI container, and the hosted `unity-build.yml` stays free of repo-specific values.
|
|
39
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# unity-devkit
|
|
2
2
|
|
|
3
|
-
Unity project discovery, local builds, CI builds, license activation, and the shared CI helpers those need.
|
|
3
|
+
Unity project discovery, local builds, CI builds, license activation, and the shared CI helpers those need. Projects are discovered structurally: any directory containing `ProjectSettings/ProjectVersion.txt` (the marker Unity itself writes) is a project; a `unity-build.json` manifest beside it optionally declares build intent (`builds`, `execute_methods`, `package`, `grant_permissions`, `tag_prefix`). Path-only commands (`lock-unity`, `test-unity`) work on any discovered project; build commands require the manifest's fields. Paired with the reusable [`unity-build.yml`](https://github.com/outernet-foundation/unity-devkit/blob/main/.github/workflows/unity-build.yml) GitHub Actions workflow hosted here — consumers call it cross-repo pinned to a pushed SHA with `secrets: inherit`.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ uv sync
|
|
|
12
12
|
|
|
13
13
|
## Commands
|
|
14
14
|
|
|
15
|
-
Run from a repo root containing one or more `
|
|
15
|
+
Run from a repo root containing one or more Unity projects (directories with `ProjectSettings/ProjectVersion.txt`).
|
|
16
16
|
|
|
17
17
|
| `uv run <name>` | What it does |
|
|
18
18
|
|---|---|
|
|
@@ -36,7 +36,7 @@ Install from PyPI:
|
|
|
36
36
|
dependencies = ["unity-devkit>=0.1.0"]
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Then `uv run compile-unity`, `uv run install`, etc. work from that repo against its own `unity-build.json`
|
|
39
|
+
Then `uv run compile-unity`, `uv run install`, etc. work from that repo against its own Unity projects (add a `unity-build.json` where build intent is needed). To test an unreleased change, pin the repo at a git ref in a scratch branch instead (`unity-devkit = { git = "…", rev = "<sha>" }` under `[tool.uv.sources]`) and drop the pin when the release lands.
|
|
40
40
|
|
|
41
41
|
## Development
|
|
42
42
|
|
|
@@ -44,4 +44,5 @@ Then `uv run compile-unity`, `uv run install`, etc. work from that repo against
|
|
|
44
44
|
uv run ruff check .
|
|
45
45
|
uv run ruff format --check .
|
|
46
46
|
uv run basedpyright
|
|
47
|
+
uv run pytest
|
|
47
48
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unity-devkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.12"
|
|
4
4
|
description = "Unity project discovery, build, license, and CI tooling"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
dependencies = [
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[dependency-groups]
|
|
15
|
-
dev = ["basedpyright>=1.39.10", "ruff>=0.14.11"]
|
|
15
|
+
dev = ["basedpyright>=1.39.10", "ruff>=0.14.11", "pytest>=8.0.0"]
|
|
16
16
|
|
|
17
17
|
[project.scripts]
|
|
18
18
|
activate-unity-license = "unity_devkit.license:activate_app"
|
|
@@ -38,7 +38,10 @@ include = ["src/unity_devkit/py.typed"]
|
|
|
38
38
|
venvPath = "."
|
|
39
39
|
venv = ".venv"
|
|
40
40
|
typeCheckingMode = "strict"
|
|
41
|
-
include = ["src"]
|
|
41
|
+
include = ["src", "tests"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
42
45
|
|
|
43
46
|
[tool.python-devkit.preflight]
|
|
44
|
-
tests =
|
|
47
|
+
tests = true
|
|
@@ -10,7 +10,9 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
10
10
|
|
|
11
11
|
@app.command()
|
|
12
12
|
def compile_unity(
|
|
13
|
-
project: Annotated[
|
|
13
|
+
project: Annotated[
|
|
14
|
+
str, typer.Option(help="Unity project name (directory containing ProjectSettings/ProjectVersion.txt)")
|
|
15
|
+
],
|
|
14
16
|
build: Annotated[str, typer.Option(help="Build target from the project's builds list (e.g. android-mobile)")],
|
|
15
17
|
) -> None:
|
|
16
18
|
for artifact in build_unity_project(project, build):
|
|
@@ -26,6 +26,10 @@ def main() -> None:
|
|
|
26
26
|
"editor-image": f"unityci/editor:{version}-{module}-{UNITYCI_IMAGE_REVISION}",
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
if not editor_versions:
|
|
30
|
+
raise SystemExit(
|
|
31
|
+
"No projects with builds declared — unity-matrix needs at least one unity-build.json with a 'builds' list"
|
|
32
|
+
)
|
|
29
33
|
license_version = max(editor_versions)
|
|
30
34
|
print(f"matrix={json.dumps({'include': matrix})}")
|
|
31
35
|
print(f"license-image=unityci/editor:{license_version}-{LICENSE_MODULE}-{UNITYCI_IMAGE_REVISION}")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
MANIFEST_FILENAME = "unity-build.json"
|
|
9
|
+
PROJECT_MARKER = Path("ProjectSettings") / "ProjectVersion.txt"
|
|
10
|
+
PRUNE_DIRECTORIES = {".git", "Library", "Temp", "obj", "Build", "node_modules", "__pycache__"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnityBuildManifest(BaseModel, extra="forbid"):
|
|
14
|
+
builds: list[str] | None = None
|
|
15
|
+
execute_methods: dict[str, str] | None = None
|
|
16
|
+
package: str | None = None
|
|
17
|
+
grant_permissions: list[str] = []
|
|
18
|
+
tag_prefix: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UnityProject(UnityBuildManifest):
|
|
22
|
+
path: Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_unity_projects() -> dict[str, UnityProject]:
|
|
26
|
+
root = Path.cwd()
|
|
27
|
+
project_directories = {directory.resolve() for directory in unity_project_directories(root)}
|
|
28
|
+
manifest_directories = {directory.resolve() for directory in directories_containing(root, MANIFEST_FILENAME)}
|
|
29
|
+
|
|
30
|
+
projects: dict[str, UnityProject] = {}
|
|
31
|
+
for project_path in sorted(project_directories | manifest_directories):
|
|
32
|
+
if project_path not in project_directories:
|
|
33
|
+
raise SystemExit(
|
|
34
|
+
f"{project_path / MANIFEST_FILENAME} is not inside a Unity project: missing {PROJECT_MARKER}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
manifest_path = project_path / MANIFEST_FILENAME
|
|
38
|
+
if manifest_path.exists():
|
|
39
|
+
manifest = UnityBuildManifest(**json.loads(manifest_path.read_text()))
|
|
40
|
+
else:
|
|
41
|
+
manifest = UnityBuildManifest()
|
|
42
|
+
|
|
43
|
+
name = project_path.name
|
|
44
|
+
if name in projects:
|
|
45
|
+
raise SystemExit(f"Duplicate Unity project name '{name}': {projects[name].path} and {project_path}")
|
|
46
|
+
projects[name] = UnityProject(path=project_path, **manifest.model_dump())
|
|
47
|
+
|
|
48
|
+
if not projects:
|
|
49
|
+
raise SystemExit(
|
|
50
|
+
f"No Unity projects found under {root} — a project is a directory containing {PROJECT_MARKER}. Run from the repo root"
|
|
51
|
+
)
|
|
52
|
+
return projects
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def visible_directories(root: Path) -> Iterator[Path]:
|
|
56
|
+
for directory, subdirectories, _filenames in os.walk(root):
|
|
57
|
+
subdirectories[:] = sorted(
|
|
58
|
+
name for name in subdirectories if name not in PRUNE_DIRECTORIES and not name.startswith(".")
|
|
59
|
+
)
|
|
60
|
+
yield Path(directory)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def unity_project_directories(root: Path) -> list[Path]:
|
|
64
|
+
return [directory for directory in visible_directories(root) if (directory / PROJECT_MARKER).is_file()]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def directories_containing(root: Path, filename: str) -> list[Path]:
|
|
68
|
+
return [directory for directory in visible_directories(root) if (directory / filename).is_file()]
|
|
@@ -13,7 +13,9 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
13
13
|
|
|
14
14
|
@app.command()
|
|
15
15
|
def main(
|
|
16
|
-
project: Annotated[
|
|
16
|
+
project: Annotated[
|
|
17
|
+
str, typer.Option(help="Unity project name (directory containing ProjectSettings/ProjectVersion.txt)")
|
|
18
|
+
],
|
|
17
19
|
test_platform: Annotated[str, typer.Option(help="Unity test platform (EditMode or PlayMode)")] = "EditMode",
|
|
18
20
|
results: Annotated[Path, typer.Option(help="Output path for NUnit XML results")] = Path(
|
|
19
21
|
"artifacts/unity-test-results.xml"
|
|
@@ -141,13 +141,18 @@ def resolve_unity_build(project: str, build: str) -> tuple[UnityProject, str, st
|
|
|
141
141
|
|
|
142
142
|
project_config = projects[project]
|
|
143
143
|
valid_builds = project_config.builds or []
|
|
144
|
+
if not valid_builds:
|
|
145
|
+
raise SystemExit(
|
|
146
|
+
f"Project '{project}' declares no builds — add a unity-build.json with a 'builds' list to {project_config.path}"
|
|
147
|
+
)
|
|
144
148
|
if build not in valid_builds:
|
|
145
|
-
|
|
146
|
-
raise SystemExit(f"Unknown build '{build}' for project '{project}'. Valid: {valid}")
|
|
149
|
+
raise SystemExit(f"Unknown build '{build}' for project '{project}'. Valid: {', '.join(valid_builds)}")
|
|
147
150
|
|
|
148
151
|
execute_method = (project_config.execute_methods or {}).get(build)
|
|
149
152
|
if not execute_method:
|
|
150
|
-
raise SystemExit(
|
|
153
|
+
raise SystemExit(
|
|
154
|
+
f"No execute method for '{build}' — declare 'execute_methods' in {project_config.path / 'unity-build.json'}"
|
|
155
|
+
)
|
|
151
156
|
|
|
152
157
|
if build not in PLATFORM_CONFIGS:
|
|
153
158
|
raise SystemExit(f"No platform config for build '{build}'. Valid: {', '.join(PLATFORM_CONFIGS)}")
|
|
@@ -160,5 +165,9 @@ def prepare_unity_project(project_path: Path) -> None:
|
|
|
160
165
|
if stale_lockfile.exists():
|
|
161
166
|
stale_lockfile.unlink()
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
# NuGetForUnity restore applies only to NuGet consumers (Assets/packages.config is
|
|
169
|
+
# their manifest); elsewhere it would demand the dotnet SDK + tool manifest and
|
|
170
|
+
# litter Assets/ with NuGet.config/packages.config/Packages.meta scaffolding.
|
|
171
|
+
if (project_path / "Assets" / "packages.config").exists():
|
|
172
|
+
bash("dotnet tool restore")
|
|
173
|
+
bash(f"dotnet nugetforunity restore {project_path}")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from unity_devkit import unity
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_prepare_skips_nuget_restore_without_packages_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
9
|
+
calls: list[str] = []
|
|
10
|
+
monkeypatch.setattr(unity, "bash", calls.append)
|
|
11
|
+
(tmp_path / "Assets").mkdir()
|
|
12
|
+
|
|
13
|
+
unity.prepare_unity_project(tmp_path)
|
|
14
|
+
|
|
15
|
+
assert calls == []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_prepare_runs_nuget_restore_for_nuget_consumer(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
19
|
+
calls: list[str] = []
|
|
20
|
+
monkeypatch.setattr(unity, "bash", calls.append)
|
|
21
|
+
(tmp_path / "Assets").mkdir()
|
|
22
|
+
(tmp_path / "Assets" / "packages.config").write_text('<?xml version="1.0"?>\n')
|
|
23
|
+
|
|
24
|
+
unity.prepare_unity_project(tmp_path)
|
|
25
|
+
|
|
26
|
+
assert calls == ["dotnet tool restore", f"dotnet nugetforunity restore {tmp_path}"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_prepare_clears_stale_unity_lockfile(tmp_path: Path) -> None:
|
|
30
|
+
lockfile = tmp_path / "Temp" / "UnityLockfile"
|
|
31
|
+
lockfile.parent.mkdir()
|
|
32
|
+
lockfile.write_text("")
|
|
33
|
+
|
|
34
|
+
unity.prepare_unity_project(tmp_path)
|
|
35
|
+
|
|
36
|
+
assert not lockfile.exists()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from unity_devkit.projects import load_unity_projects
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_unity_project(root: Path, name: str, manifest: dict[str, object] | None = None) -> Path:
|
|
10
|
+
project = root / name
|
|
11
|
+
(project / "ProjectSettings").mkdir(parents=True)
|
|
12
|
+
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
13
|
+
if manifest is not None:
|
|
14
|
+
(project / "unity-build.json").write_text(json.dumps(manifest))
|
|
15
|
+
return project
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_structural_project_without_manifest(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
19
|
+
create_unity_project(tmp_path, "Alpha")
|
|
20
|
+
monkeypatch.chdir(tmp_path)
|
|
21
|
+
|
|
22
|
+
projects = load_unity_projects()
|
|
23
|
+
|
|
24
|
+
assert set(projects) == {"Alpha"}
|
|
25
|
+
assert projects["Alpha"].path == tmp_path.resolve() / "Alpha"
|
|
26
|
+
assert projects["Alpha"].builds is None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_manifest_overlays_structural_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
30
|
+
create_unity_project(tmp_path, "Alpha", {"builds": ["linux64"], "tag_prefix": "alpha"})
|
|
31
|
+
monkeypatch.chdir(tmp_path)
|
|
32
|
+
|
|
33
|
+
projects = load_unity_projects()
|
|
34
|
+
|
|
35
|
+
assert projects["Alpha"].builds == ["linux64"]
|
|
36
|
+
assert projects["Alpha"].tag_prefix == "alpha"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_stray_manifest_outside_unity_project_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
40
|
+
stray = tmp_path / "Beta"
|
|
41
|
+
stray.mkdir()
|
|
42
|
+
(stray / "unity-build.json").write_text("{}")
|
|
43
|
+
monkeypatch.chdir(tmp_path)
|
|
44
|
+
|
|
45
|
+
with pytest.raises(SystemExit, match="not inside a Unity project"):
|
|
46
|
+
load_unity_projects()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_no_projects_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
50
|
+
monkeypatch.chdir(tmp_path)
|
|
51
|
+
|
|
52
|
+
with pytest.raises(SystemExit, match="No Unity projects found"):
|
|
53
|
+
load_unity_projects()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_duplicate_project_names_fail(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
57
|
+
create_unity_project(tmp_path / "site-a", "Alpha")
|
|
58
|
+
create_unity_project(tmp_path / "site-b", "Alpha")
|
|
59
|
+
monkeypatch.chdir(tmp_path)
|
|
60
|
+
|
|
61
|
+
with pytest.raises(SystemExit, match="Duplicate Unity project name"):
|
|
62
|
+
load_unity_projects()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_pruned_directories_are_skipped(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
66
|
+
vendored = tmp_path / "vendor" / "Library" / "SomeProject"
|
|
67
|
+
(vendored / "ProjectSettings").mkdir(parents=True)
|
|
68
|
+
(vendored / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
69
|
+
create_unity_project(tmp_path, "Real")
|
|
70
|
+
monkeypatch.chdir(tmp_path)
|
|
71
|
+
|
|
72
|
+
projects = load_unity_projects()
|
|
73
|
+
|
|
74
|
+
assert set(projects) == {"Real"}
|
|
@@ -64,6 +64,15 @@ wheels = [
|
|
|
64
64
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
65
65
|
]
|
|
66
66
|
|
|
67
|
+
[[package]]
|
|
68
|
+
name = "iniconfig"
|
|
69
|
+
version = "2.3.0"
|
|
70
|
+
source = { registry = "https://pypi.org/simple" }
|
|
71
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
72
|
+
wheels = [
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
74
|
+
]
|
|
75
|
+
|
|
67
76
|
[[package]]
|
|
68
77
|
name = "markdown-it-py"
|
|
69
78
|
version = "4.2.0"
|
|
@@ -101,6 +110,24 @@ wheels = [
|
|
|
101
110
|
{ url = "https://files.pythonhosted.org/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36", size = 39714528, upload-time = "2026-05-30T16:52:06.421Z" },
|
|
102
111
|
]
|
|
103
112
|
|
|
113
|
+
[[package]]
|
|
114
|
+
name = "packaging"
|
|
115
|
+
version = "26.3"
|
|
116
|
+
source = { registry = "https://pypi.org/simple" }
|
|
117
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
|
118
|
+
wheels = [
|
|
119
|
+
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[[package]]
|
|
123
|
+
name = "pluggy"
|
|
124
|
+
version = "1.6.0"
|
|
125
|
+
source = { registry = "https://pypi.org/simple" }
|
|
126
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
127
|
+
wheels = [
|
|
128
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
129
|
+
]
|
|
130
|
+
|
|
104
131
|
[[package]]
|
|
105
132
|
name = "pydantic"
|
|
106
133
|
version = "2.13.4"
|
|
@@ -195,6 +222,22 @@ wheels = [
|
|
|
195
222
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
196
223
|
]
|
|
197
224
|
|
|
225
|
+
[[package]]
|
|
226
|
+
name = "pytest"
|
|
227
|
+
version = "9.1.1"
|
|
228
|
+
source = { registry = "https://pypi.org/simple" }
|
|
229
|
+
dependencies = [
|
|
230
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
231
|
+
{ name = "iniconfig" },
|
|
232
|
+
{ name = "packaging" },
|
|
233
|
+
{ name = "pluggy" },
|
|
234
|
+
{ name = "pygments" },
|
|
235
|
+
]
|
|
236
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
|
237
|
+
wheels = [
|
|
238
|
+
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
|
239
|
+
]
|
|
240
|
+
|
|
198
241
|
[[package]]
|
|
199
242
|
name = "python-dotenv"
|
|
200
243
|
version = "1.2.2"
|
|
@@ -302,6 +345,7 @@ dependencies = [
|
|
|
302
345
|
[package.dev-dependencies]
|
|
303
346
|
dev = [
|
|
304
347
|
{ name = "basedpyright" },
|
|
348
|
+
{ name = "pytest" },
|
|
305
349
|
{ name = "ruff" },
|
|
306
350
|
]
|
|
307
351
|
|
|
@@ -317,5 +361,6 @@ requires-dist = [
|
|
|
317
361
|
[package.metadata.requires-dev]
|
|
318
362
|
dev = [
|
|
319
363
|
{ name = "basedpyright", specifier = ">=1.39.10" },
|
|
364
|
+
{ name = "pytest", specifier = ">=8.0.0" },
|
|
320
365
|
{ name = "ruff", specifier = ">=0.14.11" },
|
|
321
366
|
]
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel
|
|
6
|
-
|
|
7
|
-
MANIFEST_FILENAME = "unity-build.json"
|
|
8
|
-
PRUNE_DIRECTORIES = {".git", "Library", "Temp", "obj", "Build", "node_modules", "__pycache__"}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class UnityBuildManifest(BaseModel, extra="forbid"):
|
|
12
|
-
builds: list[str] | None = None
|
|
13
|
-
execute_methods: dict[str, str] | None = None
|
|
14
|
-
package: str | None = None
|
|
15
|
-
grant_permissions: list[str] = []
|
|
16
|
-
tag_prefix: str | None = None
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class UnityProject(UnityBuildManifest):
|
|
20
|
-
path: Path
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def load_unity_projects() -> dict[str, UnityProject]:
|
|
24
|
-
projects: dict[str, UnityProject] = {}
|
|
25
|
-
for directory in directories_containing(Path.cwd(), MANIFEST_FILENAME):
|
|
26
|
-
project_path = directory.resolve()
|
|
27
|
-
name = project_path.name
|
|
28
|
-
version_file = project_path / "ProjectSettings" / "ProjectVersion.txt"
|
|
29
|
-
if not version_file.exists():
|
|
30
|
-
raise SystemExit(
|
|
31
|
-
f"{project_path / MANIFEST_FILENAME} is not inside a Unity project: missing {version_file}"
|
|
32
|
-
)
|
|
33
|
-
if name in projects:
|
|
34
|
-
raise SystemExit(f"Duplicate Unity project name '{name}': {projects[name].path} and {project_path}")
|
|
35
|
-
|
|
36
|
-
manifest = UnityBuildManifest(**json.loads((project_path / MANIFEST_FILENAME).read_text()))
|
|
37
|
-
projects[name] = UnityProject(path=project_path, **manifest.model_dump())
|
|
38
|
-
|
|
39
|
-
if not projects:
|
|
40
|
-
raise SystemExit(f"No {MANIFEST_FILENAME} manifests found under {Path.cwd()} — run from the repo root")
|
|
41
|
-
|
|
42
|
-
return projects
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def directories_containing(root: Path, filename: str) -> list[Path]:
|
|
46
|
-
directories: list[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
|
-
if filename in filenames:
|
|
52
|
-
directories.append(Path(directory))
|
|
53
|
-
return 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|