unity-devkit 0.1.13__tar.gz → 0.1.14__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.13 → unity_devkit-0.1.14}/.github/workflows/release.yml +1 -1
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/AGENTS.md +8 -8
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/PKG-INFO +2 -2
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/README.md +3 -3
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/publish-config.json +1 -2
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/pyproject.toml +2 -2
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/build_unity.py +16 -5
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/compile_unity.py +1 -3
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/install.py +4 -4
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/lock_unity.py +2 -2
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/matrix.py +3 -3
- unity_devkit-0.1.14/src/unity_devkit/projects.py +59 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/test_unity.py +3 -5
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/unity.py +5 -5
- unity_devkit-0.1.14/tests/test_latest_tag_version.py +19 -0
- unity_devkit-0.1.14/tests/test_matrix.py +46 -0
- unity_devkit-0.1.14/tests/test_projects.py +90 -0
- unity_devkit-0.1.14/tests/test_resolve.py +56 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/uv.lock +6 -6
- unity_devkit-0.1.13/src/unity_devkit/projects.py +0 -68
- unity_devkit-0.1.13/tests/test_projects.py +0 -74
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/.gitattributes +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/.github/workflows/ci.yml +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/.github/workflows/unity-build.yml +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/.gitignore +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/CLAUDE.md +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/LICENSE +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/NOTICE +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/ruff.base.toml +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/ruff.toml +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/__init__.py +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/license.py +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/license_restore.py +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/src/unity_devkit/py.typed +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.14}/tests/test_prepare.py +0 -0
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## What this is
|
|
4
4
|
|
|
5
|
-
`unity-devkit` is the Unity build toolkit: project
|
|
5
|
+
`unity-devkit` is the Unity build toolkit: the root project catalog, local builds, CI builds, license activation, the `install` command (download-or-build then install onto a device), and the ORAS cache/setup helpers those need. Consumer repositories install this package from PyPI and get the same Unity CI and install flow — paired with the reusable `unity-build.yml` GitHub Actions workflow hosted in this repo's `.github/workflows/`, which invokes only this package's entry points inside the caller's checkout.
|
|
6
6
|
|
|
7
7
|
The repo and package renamed from `unity-buildkit` to `unity-devkit` (2026-09-21, member of the `-devkit` family; before that `placeframe-unity` → `unity-buildkit` on extraction). The PyPI identity is fresh: `unity-devkit` starts its own tag ledger at `0.1.0`; the terminal `unity-buildkit` distributions (≤0.1.1) are deprecation signposts pointing here, not this package's history.
|
|
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-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).
|
|
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). While pre-1.0, breaking changes ride the current `0.1` patch line; a `major_minor` bump is reserved for the eventual 1.0.0 stabilization release.
|
|
12
12
|
|
|
13
13
|
## Shape
|
|
14
14
|
|
|
@@ -16,8 +16,8 @@ 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>`, where the project's `unity-
|
|
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
|
|
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-devkit.json` catalog entry declares `builds` and `execute_methods`. Streams the editor log, prints output paths under `<project>/Build/`. |
|
|
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 catalog entry'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. |
|
|
23
23
|
| `activate-unity-license` | `license.py` | Activate the Unity Editor license (locally or with `--oras-push` for the CI cache). |
|
|
@@ -25,7 +25,7 @@ One flat module per concern under `src/unity_devkit/`:
|
|
|
25
25
|
| `unity-matrix` | `matrix.py` | Emit the CI build matrix (CI-only). |
|
|
26
26
|
| `build-unity` | `build_unity.py` | CI build with library cache restore/save and version stamping (CI-only; assumes `GITHUB_WORKSPACE`, OCI registry, runner environment). |
|
|
27
27
|
|
|
28
|
-
Supporting modules: `projects.py` (
|
|
28
|
+
Supporting modules: `projects.py` (catalog schema + loader, and the pruned tree walk that is cross-repo API), `unity.py` (editor lookup, platform configs, batchmode command, shared Unity runner). The CI-floor modules (step wrapper, runner provisioning, ORAS artifact cache) live in [`ci-devkit`](https://github.com/outernet-foundation/ci-devkit) (a runtime dependency); unity-devkit owns only Unity concerns.
|
|
29
29
|
|
|
30
30
|
## Constraints
|
|
31
31
|
|
|
@@ -33,15 +33,15 @@ 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
|
|
36
|
+
**Unity projects are declared in a root catalog.** `unity-devkit.json` at the repo root maps stable project names to entries (`path`, `builds`, `execute_methods`, `package`, `grant_permissions`, `tag_prefix` — schema in `projects.py`). Every verb resolves `--project` through the catalog, the path-only ones (`lock-unity`, `test-unity`) included — absence from the catalog is the exclusion mechanism: a project not listed is never touched, so vendored or nested projects need no prune list. Names are catalog keys, decoupled from directory names; they are the workflow contract (`matrix.project-name`, artifact names, `--project` everywhere). `ProjectSettings/ProjectVersion.txt` inside each entry's project remains the editor-version truth, read per entry; an entry whose path lacks the marker fails at load, as does a missing or empty catalog. Run commands from the repo root — catalog paths are repo-relative. The per-project `unity-build.json` manifests and the structural marker walk are gone from the verbs; the pruned walk (`projects.directories_containing`) remains as cross-repo API.
|
|
37
37
|
|
|
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
|
|
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 cataloged 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
|
|
|
40
40
|
**Entry-point names are the workflow contract.** The hosted `unity-build.yml` invokes `unity-matrix`, `unity-license-tag`, `activate-unity-license`, and `build-unity` by name via `uv run` in the caller's checkout (the caller's venv supplies this package). Consumers pin the workflow by pushed SHA and the package by PyPI version independently; the two pins stay compatible as long as the entry-point names and flags hold, so treat those as a public API.
|
|
41
41
|
|
|
42
42
|
**The environment-lookup helpers are cross-repo Python API.** `unity.find_editor_for_version` (editor ladder: `unity-editor` on PATH → `/opt/unity/<version>` → `~/Unity/Hub/Editor/<version>`; raises `ValueError` listing every searched path on a miss), `unity.editor_version` (`ProjectVersion.txt` parse; `None` for a missing file or unparseable content), and `projects.directories_containing` (+ `PRUNE_DIRECTORIES`) are imported by registry consumers — prepo's `sync` verb. Their signatures and miss behavior are contract, not internals. The `SystemExit`-raising wrapper `read_editor_version` exists for unity-devkit's own CLI paths and delegates to the core helper; `unity_batchmode_command` consumes the `ValueError`-raising `find_editor_for_version` and `read_editor_version` directly, so a missing editor surfaces as the raw `ValueError`.
|
|
43
43
|
|
|
44
|
-
**Version stamping is opt-in via `tag_prefix`.** `build-unity` writes `.build-version.json` (consumed by the project's build `executeMethod`) only when the project's
|
|
44
|
+
**Version stamping is opt-in via `tag_prefix`.** `build-unity` writes `.build-version.json` (consumed by the project's build `executeMethod`) only when the project's catalog entry declares `tag_prefix`; the version is the latest `<tag_prefix>-v*` git tag (a local bashrun query — the version-ledger primitives live in release-devkit and are not imported here) plus a run-number suffix. Projects without the field build unversioned. The catalog's `tag_prefix` and the same package's `tag_prefix` in the repo's `release-devkit.json` are two spellings of one fact — they must agree for projects that release.
|
|
45
45
|
|
|
46
46
|
**The ORAS cache media type identifies the wrapper package, not the consuming repo.** `cache.save()` pushes with `application/vnd.unity-devkit.cache.v1+zstd`. Restore doesn't filter on media type, so older manifests carrying a different vendor prefix still pull, but new pushes always carry this one — the identifier tracks the tool that wrote the cache, not the project whose bytes are inside it.
|
|
47
47
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unity-devkit
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Unity
|
|
3
|
+
Version: 0.1.14
|
|
4
|
+
Summary: Unity build, license, and CI tooling
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
License-File: NOTICE
|
|
7
7
|
Requires-Python: >=3.13
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# unity-devkit
|
|
2
2
|
|
|
3
|
-
Unity
|
|
3
|
+
Unity build, license, and CI tooling. Projects are declared in a root `unity-devkit.json` catalog: each entry maps a stable project name to its path plus optional build intent (`builds`, `execute_methods`, `package`, `grant_permissions`, `tag_prefix`). Every command resolves projects through the catalog — absence from the catalog is the exclusion mechanism — and `ProjectSettings/ProjectVersion.txt` inside each project remains the editor-version truth. 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
|
|
15
|
+
Run from a repo root with a `unity-devkit.json` catalog declaring its Unity projects.
|
|
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 projects
|
|
39
|
+
Then `uv run compile-unity`, `uv run install`, etc. work from that repo against its own Unity projects — declare them in a root `unity-devkit.json` catalog (entries need `builds` + `execute_methods` 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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unity-devkit"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "Unity
|
|
3
|
+
version = "0.1.14"
|
|
4
|
+
description = "Unity build, license, and CI tooling"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"bashrun>=0.1.0",
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Annotated
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
|
+
from bashrun.bash import bash_output
|
|
10
11
|
from pydantic_settings import BaseSettings
|
|
11
12
|
|
|
12
13
|
from ci_devkit.cache import restore, save
|
|
@@ -15,15 +16,12 @@ from .license_restore import restore_license
|
|
|
15
16
|
from ci_devkit.setup import configure_git, install_dotnet
|
|
16
17
|
from ci_devkit.setup_oras import install_oras
|
|
17
18
|
from .unity import prepare_unity_project, resolve_unity_build, run_unity_batchmode
|
|
18
|
-
from ci_devkit.git_tags import get_latest_tag_version
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class Settings(BaseSettings):
|
|
22
22
|
github_workspace: str
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
settings = Settings.model_validate({})
|
|
26
|
-
|
|
27
25
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
28
26
|
|
|
29
27
|
|
|
@@ -40,6 +38,8 @@ def main(
|
|
|
40
38
|
str, typer.Option(help="Newline-separated KEY=VALUE pairs injected into the Unity build process environment")
|
|
41
39
|
] = "",
|
|
42
40
|
) -> None:
|
|
41
|
+
settings = Settings.model_validate({})
|
|
42
|
+
|
|
43
43
|
for line in build_env.splitlines():
|
|
44
44
|
entry = line.strip()
|
|
45
45
|
if not entry:
|
|
@@ -66,14 +66,18 @@ def main(
|
|
|
66
66
|
with ci_step("Prepare build"):
|
|
67
67
|
project_config, build_flag, execute_method = resolve_unity_build(project, platform)
|
|
68
68
|
unity_project_path = project_config.path
|
|
69
|
+
if unity_project_path.resolve() != project_path.resolve():
|
|
70
|
+
raise SystemExit(
|
|
71
|
+
f"--project-path {project_path} does not match catalog entry '{project}' at {unity_project_path}"
|
|
72
|
+
)
|
|
69
73
|
|
|
70
74
|
with ci_step("Prepare project"):
|
|
71
75
|
prepare_unity_project(unity_project_path)
|
|
72
76
|
|
|
73
|
-
with ci_step(f"Build {
|
|
77
|
+
with ci_step(f"Build {project} [{platform}]"):
|
|
74
78
|
tag_prefix = project_config.tag_prefix
|
|
75
79
|
if tag_prefix:
|
|
76
|
-
version =
|
|
80
|
+
version = latest_tag_version(f"{tag_prefix}-v") or "0.0.0"
|
|
77
81
|
full_version = f"{version}-dev+{run_number}" if branch != "main" else f"{version}+{run_number}"
|
|
78
82
|
version_file = unity_project_path / ".build-version.json"
|
|
79
83
|
version_file.write_text(json.dumps({"version": full_version, "runNumber": run_number}))
|
|
@@ -100,3 +104,10 @@ def main(
|
|
|
100
104
|
for file in build_directory.rglob("*"):
|
|
101
105
|
if file.suffix in {".apk", ".exe"}:
|
|
102
106
|
shutil.copy2(file, artifact_directory / file.name)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def latest_tag_version(prefix: str) -> str | None:
|
|
110
|
+
output = bash_output(f'git tag --list "{prefix}*" --sort=-v:refname').strip()
|
|
111
|
+
if not output:
|
|
112
|
+
return None
|
|
113
|
+
return output.splitlines()[0][len(prefix) :]
|
|
@@ -10,9 +10,7 @@ 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[
|
|
14
|
-
str, typer.Option(help="Unity project name (directory containing ProjectSettings/ProjectVersion.txt)")
|
|
15
|
-
],
|
|
13
|
+
project: Annotated[str, typer.Option(help="Unity project name (catalog key in unity-devkit.json)")],
|
|
16
14
|
build: Annotated[str, typer.Option(help="Build target from the project's builds list (e.g. android-mobile)")],
|
|
17
15
|
) -> None:
|
|
18
16
|
for artifact in build_unity_project(project, build):
|
|
@@ -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_catalog
|
|
13
13
|
|
|
14
14
|
INSTALLABLE_TARGETS = {"android-mobile", "magicleap", "linux64"}
|
|
15
15
|
ADB_TARGETS = {"android-mobile", "magicleap"}
|
|
@@ -37,7 +37,7 @@ def main(
|
|
|
37
37
|
"--no-grant-permissions",
|
|
38
38
|
help=(
|
|
39
39
|
"Skip the post-install `adb shell pm grant` calls listed under `grant_permissions` "
|
|
40
|
-
"for the project in
|
|
40
|
+
"for the project's entry in unity-devkit.json. Permissions are granted by default."
|
|
41
41
|
),
|
|
42
42
|
),
|
|
43
43
|
] = False,
|
|
@@ -53,7 +53,7 @@ def main(
|
|
|
53
53
|
),
|
|
54
54
|
] = False,
|
|
55
55
|
) -> None:
|
|
56
|
-
projects =
|
|
56
|
+
projects = load_catalog()
|
|
57
57
|
|
|
58
58
|
project_name = next((name for name in projects if name.lower() == project.lower()), None)
|
|
59
59
|
if project_name is None:
|
|
@@ -156,7 +156,7 @@ def main(
|
|
|
156
156
|
if permissions and not no_grant_permissions:
|
|
157
157
|
if not package:
|
|
158
158
|
raise typer.BadParameter(
|
|
159
|
-
f"{project_name} has grant_permissions but no 'package' field in unity-
|
|
159
|
+
f"{project_name} has grant_permissions but no 'package' field in its unity-devkit.json entry"
|
|
160
160
|
)
|
|
161
161
|
for permission in permissions:
|
|
162
162
|
print(f"Granting {permission} to {package}")
|
|
@@ -2,7 +2,7 @@ from typing import Annotated
|
|
|
2
2
|
|
|
3
3
|
import typer
|
|
4
4
|
|
|
5
|
-
from .projects import
|
|
5
|
+
from .projects import load_catalog
|
|
6
6
|
from .unity 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 = load_catalog()
|
|
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 load_catalog
|
|
5
5
|
from .unity import LICENSE_MODULE, PLATFORM_CONFIGS, UNITYCI_IMAGE_REVISION, read_editor_version
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def main() -> None:
|
|
9
|
-
projects =
|
|
9
|
+
projects = load_catalog()
|
|
10
10
|
matrix: list[dict[str, str]] = []
|
|
11
11
|
editor_versions: set[str] = set()
|
|
12
12
|
|
|
@@ -28,7 +28,7 @@ def main() -> None:
|
|
|
28
28
|
|
|
29
29
|
if not editor_versions:
|
|
30
30
|
raise SystemExit(
|
|
31
|
-
"No projects with builds declared — unity-matrix needs at least one unity-
|
|
31
|
+
"No projects with builds declared — unity-matrix needs at least one unity-devkit.json entry with a 'builds' list"
|
|
32
32
|
)
|
|
33
33
|
license_version = max(editor_versions)
|
|
34
34
|
print(f"matrix={json.dumps({'include': matrix})}")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, 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
|
+
execute_methods: dict[str, str] | None = None
|
|
18
|
+
package: str | None = None
|
|
19
|
+
grant_permissions: list[str] = Field(default_factory=list)
|
|
20
|
+
tag_prefix: str | None = None
|
|
21
|
+
|
|
22
|
+
@field_validator("path")
|
|
23
|
+
@classmethod
|
|
24
|
+
def anchor_relative_path(cls, value: Path) -> Path:
|
|
25
|
+
return value if value.is_absolute() else Path.cwd() / value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
CATALOG_ADAPTER = TypeAdapter(dict[str, CatalogEntry])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_catalog() -> dict[str, CatalogEntry]:
|
|
32
|
+
catalog_path = Path.cwd() / CATALOG_FILENAME
|
|
33
|
+
if not catalog_path.is_file():
|
|
34
|
+
raise SystemExit(
|
|
35
|
+
f"No {CATALOG_FILENAME} catalog at {catalog_path} — declare Unity projects as catalog entries at the repo root"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
catalog = CATALOG_ADAPTER.validate_json(catalog_path.read_text(encoding="utf-8"))
|
|
39
|
+
if not catalog:
|
|
40
|
+
raise SystemExit(f"{catalog_path} declares no projects — at least one catalog entry is required")
|
|
41
|
+
|
|
42
|
+
for name, entry in catalog.items():
|
|
43
|
+
if not (entry.path / PROJECT_MARKER).is_file():
|
|
44
|
+
raise SystemExit(
|
|
45
|
+
f"Catalog entry '{name}' points at {entry.path}, which is not a Unity project: missing {PROJECT_MARKER}"
|
|
46
|
+
)
|
|
47
|
+
return catalog
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def visible_directories(root: Path) -> Iterator[Path]:
|
|
51
|
+
for directory, subdirectories, _filenames in os.walk(root):
|
|
52
|
+
subdirectories[:] = sorted(
|
|
53
|
+
name for name in subdirectories if name not in PRUNE_DIRECTORIES and not name.startswith(".")
|
|
54
|
+
)
|
|
55
|
+
yield Path(directory)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def directories_containing(root: Path, filename: str) -> list[Path]:
|
|
59
|
+
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 load_catalog
|
|
9
9
|
from .unity import run_unity_batchmode
|
|
10
10
|
|
|
11
11
|
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
@@ -13,15 +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[
|
|
17
|
-
str, typer.Option(help="Unity project name (directory containing ProjectSettings/ProjectVersion.txt)")
|
|
18
|
-
],
|
|
16
|
+
project: Annotated[str, typer.Option(help="Unity project name (catalog key in unity-devkit.json)")],
|
|
19
17
|
test_platform: Annotated[str, typer.Option(help="Unity test platform (EditMode or PlayMode)")] = "EditMode",
|
|
20
18
|
results: Annotated[Path, typer.Option(help="Output path for NUnit XML results")] = Path(
|
|
21
19
|
"artifacts/unity-test-results.xml"
|
|
22
20
|
),
|
|
23
21
|
) -> None:
|
|
24
|
-
projects =
|
|
22
|
+
projects = load_catalog()
|
|
25
23
|
if project not in projects:
|
|
26
24
|
raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
|
|
27
25
|
|
|
@@ -6,7 +6,7 @@ from typing import TypedDict
|
|
|
6
6
|
|
|
7
7
|
from bashrun.bash import CalledProcessError, bash, bash_pipe
|
|
8
8
|
|
|
9
|
-
from .projects import
|
|
9
|
+
from .projects import CatalogEntry, load_catalog
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class PlatformConfig(TypedDict):
|
|
@@ -134,8 +134,8 @@ def editor_version(project_path: Path) -> str | None:
|
|
|
134
134
|
return None
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
def resolve_unity_build(project: str, build: str) -> tuple[
|
|
138
|
-
projects =
|
|
137
|
+
def resolve_unity_build(project: str, build: str) -> tuple[CatalogEntry, str, str]:
|
|
138
|
+
projects = load_catalog()
|
|
139
139
|
if project not in projects:
|
|
140
140
|
raise SystemExit(f"Unknown project '{project}'. Valid: {', '.join(projects)}")
|
|
141
141
|
|
|
@@ -143,7 +143,7 @@ def resolve_unity_build(project: str, build: str) -> tuple[UnityProject, str, st
|
|
|
143
143
|
valid_builds = project_config.builds or []
|
|
144
144
|
if not valid_builds:
|
|
145
145
|
raise SystemExit(
|
|
146
|
-
f"Project '{project}' declares no builds — add a
|
|
146
|
+
f"Project '{project}' declares no builds — add a 'builds' list to its entry in unity-devkit.json"
|
|
147
147
|
)
|
|
148
148
|
if build not in valid_builds:
|
|
149
149
|
raise SystemExit(f"Unknown build '{build}' for project '{project}'. Valid: {', '.join(valid_builds)}")
|
|
@@ -151,7 +151,7 @@ def resolve_unity_build(project: str, build: str) -> tuple[UnityProject, str, st
|
|
|
151
151
|
execute_method = (project_config.execute_methods or {}).get(build)
|
|
152
152
|
if not execute_method:
|
|
153
153
|
raise SystemExit(
|
|
154
|
-
f"No execute method for '{build}' — declare 'execute_methods'
|
|
154
|
+
f"No execute method for '{build}' — declare it under 'execute_methods' for '{project}' in unity-devkit.json"
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
if build not in PLATFORM_CONFIGS:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from bashrun.bash import bash
|
|
5
|
+
|
|
6
|
+
from unity_devkit.build_unity import latest_tag_version
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_latest_tag_version_returns_newest_version_sorted(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
10
|
+
monkeypatch.chdir(tmp_path)
|
|
11
|
+
bash("git init -q")
|
|
12
|
+
bash("git config user.email test@example.com")
|
|
13
|
+
bash("git config user.name test")
|
|
14
|
+
bash("git commit --allow-empty -m init -q")
|
|
15
|
+
for version in ("0.1.2", "0.1.9", "0.1.10"):
|
|
16
|
+
bash(f"git tag thing-v{version}")
|
|
17
|
+
|
|
18
|
+
assert latest_tag_version("thing-v") == "0.1.10"
|
|
19
|
+
assert latest_tag_version("absent-v") is None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from unity_devkit import matrix
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def write_repository(tmp_path: Path, entry: dict[str, object]) -> None:
|
|
10
|
+
project = tmp_path / "apps" / "Tool"
|
|
11
|
+
(project / "ProjectSettings").mkdir(parents=True)
|
|
12
|
+
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
13
|
+
(tmp_path / "unity-devkit.json").write_text(json.dumps({"tool": entry}))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_matrix_emits_entries_from_catalog(
|
|
17
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
18
|
+
) -> None:
|
|
19
|
+
write_repository(
|
|
20
|
+
tmp_path, {"path": "apps/Tool", "builds": ["linux64"], "execute_methods": {"linux64": "Tool.Build"}}
|
|
21
|
+
)
|
|
22
|
+
monkeypatch.chdir(tmp_path)
|
|
23
|
+
|
|
24
|
+
matrix.main()
|
|
25
|
+
|
|
26
|
+
lines = capsys.readouterr().out.splitlines()
|
|
27
|
+
include = json.loads(lines[0][len("matrix=") :])["include"]
|
|
28
|
+
assert include == [
|
|
29
|
+
{
|
|
30
|
+
"project": str(Path("apps") / "Tool"),
|
|
31
|
+
"project-name": "tool",
|
|
32
|
+
"cache-key": "tool",
|
|
33
|
+
"platform": "linux64",
|
|
34
|
+
"module": "linux-il2cpp",
|
|
35
|
+
"editor-image": "unityci/editor:6000.0.66f1-linux-il2cpp-3",
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
assert lines[1] == "license-image=unityci/editor:6000.0.66f1-linux-il2cpp-3"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_matrix_fails_without_builds(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
42
|
+
write_repository(tmp_path, {"path": "apps/Tool"})
|
|
43
|
+
monkeypatch.chdir(tmp_path)
|
|
44
|
+
|
|
45
|
+
with pytest.raises(SystemExit, match="No projects with builds declared"):
|
|
46
|
+
matrix.main()
|
|
@@ -0,0 +1,90 @@
|
|
|
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": ["linux64"], "tag_prefix": "alpha"}})
|
|
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 == ["linux64"]
|
|
33
|
+
assert projects["Alpha"].tag_prefix == "alpha"
|
|
34
|
+
assert projects["Alpha"].grant_permissions == []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_catalog_name_is_decoupled_from_directory_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
38
|
+
create_unity_project(tmp_path, "apps/capture-tool")
|
|
39
|
+
write_catalog(tmp_path, {"capture": {"path": "apps/capture-tool"}})
|
|
40
|
+
monkeypatch.chdir(tmp_path)
|
|
41
|
+
|
|
42
|
+
projects = load_catalog()
|
|
43
|
+
|
|
44
|
+
assert set(projects) == {"capture"}
|
|
45
|
+
assert projects["capture"].path == Path.cwd() / "apps" / "capture-tool"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_missing_catalog_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
49
|
+
monkeypatch.chdir(tmp_path)
|
|
50
|
+
|
|
51
|
+
with pytest.raises(SystemExit, match=r"No unity-devkit\.json catalog"):
|
|
52
|
+
load_catalog()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_empty_catalog_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
56
|
+
write_catalog(tmp_path, {})
|
|
57
|
+
monkeypatch.chdir(tmp_path)
|
|
58
|
+
|
|
59
|
+
with pytest.raises(SystemExit, match="declares no projects"):
|
|
60
|
+
load_catalog()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_entry_outside_unity_project_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
64
|
+
(tmp_path / "not-a-project").mkdir()
|
|
65
|
+
write_catalog(tmp_path, {"broken": {"path": "not-a-project"}})
|
|
66
|
+
monkeypatch.chdir(tmp_path)
|
|
67
|
+
|
|
68
|
+
with pytest.raises(SystemExit, match="not a Unity project"):
|
|
69
|
+
load_catalog()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_unknown_entry_key_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
73
|
+
create_unity_project(tmp_path, "Alpha")
|
|
74
|
+
write_catalog(tmp_path, {"Alpha": {"path": "Alpha", "unity-build": True}})
|
|
75
|
+
monkeypatch.chdir(tmp_path)
|
|
76
|
+
|
|
77
|
+
with pytest.raises(ValidationError):
|
|
78
|
+
load_catalog()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_directories_containing_honors_prune_list(tmp_path: Path) -> None:
|
|
82
|
+
vendored = tmp_path / "vendor" / "Library" / "SomeProject"
|
|
83
|
+
(vendored / "ProjectSettings").mkdir(parents=True)
|
|
84
|
+
(vendored / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
85
|
+
real = create_unity_project(tmp_path, "Real")
|
|
86
|
+
|
|
87
|
+
settings_directories = directories_containing(tmp_path, "ProjectVersion.txt")
|
|
88
|
+
|
|
89
|
+
assert real / "ProjectSettings" in settings_directories
|
|
90
|
+
assert vendored / "ProjectSettings" not in settings_directories
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from unity_devkit.unity import resolve_unity_build
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def write_repository(tmp_path: Path, entry: dict[str, object]) -> None:
|
|
10
|
+
project = tmp_path / "Tool"
|
|
11
|
+
(project / "ProjectSettings").mkdir(parents=True)
|
|
12
|
+
(project / "ProjectSettings" / "ProjectVersion.txt").write_text("m_EditorVersion: 6000.0.66f1\n")
|
|
13
|
+
(tmp_path / "unity-devkit.json").write_text(json.dumps({"tool": entry}))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_unknown_project_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
17
|
+
write_repository(tmp_path, {"path": "Tool", "builds": ["linux64"], "execute_methods": {"linux64": "Tool.Build"}})
|
|
18
|
+
monkeypatch.chdir(tmp_path)
|
|
19
|
+
|
|
20
|
+
with pytest.raises(SystemExit, match="Unknown project 'nope'"):
|
|
21
|
+
resolve_unity_build("nope", "linux64")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_project_without_builds_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
25
|
+
write_repository(tmp_path, {"path": "Tool"})
|
|
26
|
+
monkeypatch.chdir(tmp_path)
|
|
27
|
+
|
|
28
|
+
with pytest.raises(SystemExit, match="declares no builds"):
|
|
29
|
+
resolve_unity_build("tool", "linux64")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_unknown_build_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
33
|
+
write_repository(tmp_path, {"path": "Tool", "builds": ["linux64"], "execute_methods": {"linux64": "Tool.Build"}})
|
|
34
|
+
monkeypatch.chdir(tmp_path)
|
|
35
|
+
|
|
36
|
+
with pytest.raises(SystemExit, match="Unknown build 'win64'"):
|
|
37
|
+
resolve_unity_build("tool", "win64")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_missing_execute_method_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
41
|
+
write_repository(tmp_path, {"path": "Tool", "builds": ["linux64"]})
|
|
42
|
+
monkeypatch.chdir(tmp_path)
|
|
43
|
+
|
|
44
|
+
with pytest.raises(SystemExit, match="No execute method"):
|
|
45
|
+
resolve_unity_build("tool", "linux64")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_resolve_returns_platform_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
49
|
+
write_repository(tmp_path, {"path": "Tool", "builds": ["linux64"], "execute_methods": {"linux64": "Tool.Build"}})
|
|
50
|
+
monkeypatch.chdir(tmp_path)
|
|
51
|
+
|
|
52
|
+
project_config, build_flag, execute_method = resolve_unity_build("tool", "linux64")
|
|
53
|
+
|
|
54
|
+
assert project_config.path == Path.cwd() / "Tool"
|
|
55
|
+
assert build_flag == "-buildTarget StandaloneLinux64"
|
|
56
|
+
assert execute_method == "Tool.Build"
|
|
@@ -34,25 +34,25 @@ wheels = [
|
|
|
34
34
|
|
|
35
35
|
[[package]]
|
|
36
36
|
name = "bashrun"
|
|
37
|
-
version = "0.1.
|
|
37
|
+
version = "0.1.9"
|
|
38
38
|
source = { registry = "https://pypi.org/simple" }
|
|
39
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
39
|
+
sdist = { url = "https://files.pythonhosted.org/packages/61/cd/ce0a41f08e1392c9bb4ecfa9533973ce537e339c691df22acffe71860d13/bashrun-0.1.9.tar.gz", hash = "sha256:693c8b13c3576b969c28a22cebc6b39da9957de647a49d7faa36b0843feec29a", size = 22266, upload-time = "2026-09-24T02:05:10.783Z" }
|
|
40
40
|
wheels = [
|
|
41
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
41
|
+
{ url = "https://files.pythonhosted.org/packages/1f/28/ab9f3a6739d62a39827ab86015e4f44d0c0dbf2f4c171f185185ecec28cc/bashrun-0.1.9-py3-none-any.whl", hash = "sha256:abd34396256184e0f9ac7e0f45659269f3f83bcf6ad22ebec862a197e9d6d3eb", size = 7500, upload-time = "2026-09-24T02:05:09.795Z" },
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
[[package]]
|
|
45
45
|
name = "ci-devkit"
|
|
46
|
-
version = "0.1.
|
|
46
|
+
version = "0.1.10"
|
|
47
47
|
source = { registry = "https://pypi.org/simple" }
|
|
48
48
|
dependencies = [
|
|
49
49
|
{ name = "bashrun" },
|
|
50
50
|
{ name = "pydantic" },
|
|
51
51
|
{ name = "pydantic-settings" },
|
|
52
52
|
]
|
|
53
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
53
|
+
sdist = { url = "https://files.pythonhosted.org/packages/bc/19/ca0addca3fc9fdf9a9e6ac11cd8e1e831b1ea1daccda875f1350ee3d0f1d/ci_devkit-0.1.10.tar.gz", hash = "sha256:733afdf59fd94d16e9d47291c85a34e827867560e624dc41daf77848e85c1b6d", size = 40879, upload-time = "2026-09-24T02:05:32.107Z" }
|
|
54
54
|
wheels = [
|
|
55
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/f0/44/33904ce3984eab322b000687f072adf26aa213e809d3952df913c8f8d1df/ci_devkit-0.1.10-py3-none-any.whl", hash = "sha256:f0194348742cf7d13cec75204784bdcccbdf08f51f458706c04840f0e8739709", size = 26245, upload-time = "2026-09-24T02:05:30.928Z" },
|
|
56
56
|
]
|
|
57
57
|
|
|
58
58
|
[[package]]
|
|
@@ -1,68 +0,0 @@
|
|
|
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()]
|
|
@@ -1,74 +0,0 @@
|
|
|
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"}
|
|
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
|