unity-devkit 0.1.9__tar.gz → 0.1.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/.github/workflows/release.yml +6 -0
  2. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/AGENTS.md +2 -2
  3. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/PKG-INFO +1 -1
  4. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/README.md +4 -3
  5. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/pyproject.toml +7 -4
  6. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/compile_unity.py +3 -1
  7. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/matrix.py +4 -0
  8. unity_devkit-0.1.11/src/unity_devkit/projects.py +68 -0
  9. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/test_unity.py +3 -1
  10. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/unity.py +8 -3
  11. unity_devkit-0.1.11/tests/test_projects.py +74 -0
  12. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/uv.lock +45 -0
  13. unity_devkit-0.1.9/src/unity_devkit/projects.py +0 -53
  14. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/.gitattributes +0 -0
  15. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/.github/workflows/ci.yml +0 -0
  16. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/.github/workflows/unity-build.yml +0 -0
  17. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/.gitignore +0 -0
  18. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/CLAUDE.md +0 -0
  19. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/LICENSE +0 -0
  20. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/NOTICE +0 -0
  21. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/publish-config.json +0 -0
  22. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/ruff.base.toml +0 -0
  23. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/ruff.toml +0 -0
  24. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/__init__.py +0 -0
  25. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/build_unity.py +0 -0
  26. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/install.py +0 -0
  27. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/license.py +0 -0
  28. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/license_restore.py +0 -0
  29. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/lock_unity.py +0 -0
  30. {unity_devkit-0.1.9 → unity_devkit-0.1.11}/src/unity_devkit/py.typed +0 -0
@@ -6,6 +6,10 @@ on:
6
6
  types: [completed]
7
7
  branches: [main]
8
8
 
9
+ concurrency:
10
+ group: release
11
+ cancel-in-progress: false
12
+
9
13
  jobs:
10
14
  publish:
11
15
  if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main'
@@ -15,6 +19,8 @@ jobs:
15
19
  id-token: write
16
20
  steps:
17
21
  - uses: actions/checkout@v5
22
+ with:
23
+ ref: ${{ github.event.workflow_run.head_sha }}
18
24
 
19
25
  # Workaround: fetch-tags is broken with shallow clones
20
26
  - run: git fetch --tags origin
@@ -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>`, matching a discovered `unity-build.json` manifest. Streams the editor log, prints output paths under `<project>/Build/`. |
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 manifest-driven, not catalog-driven.** The Unity commands discover projects by scanning the working-directory tree for `unity-build.json` manifest files (pruning `Library`, `Temp`, `obj`, `Build`, `node_modules`, `.git`, and dot-directories). A manifest marks its directory as an opted-in Unity project; the project name is the directory name; the manifest carries only intent the project cannot otherwise express (`builds`, `execute_methods`, `package`, `grant_permissions`, `tag_prefix` — schema in `projects.py`). 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, and the commands run unchanged in any repo containing manifests. Discovery anchors at the current working directory — run commands from the repo root.
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
  Metadata-Version: 2.5
2
2
  Name: unity-devkit
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Unity project discovery, build, license, and CI tooling
5
5
  License-File: LICENSE
6
6
  License-File: NOTICE
@@ -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. Consumers scan the working-directory tree for `unity-build.json` manifests to enumerate their Unity projects; the commands here (`compile-unity`, `install`, `build-unity`, `unity-matrix`, license helpers) work unchanged in any repo containing manifests. 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`.
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 `unity-build.json` manifests.
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` projects. 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.
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.9"
3
+ version = "0.1.11"
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 = false
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[str, typer.Option(help="Unity project name (directory containing unity-build.json)")],
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[str, typer.Option(help="Unity project name (directory containing unity-build.json)")],
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
- valid = ", ".join(valid_builds) or "(none defined)"
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(f"No execute method for project '{project}' build '{build}'")
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)}")
@@ -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