unity-devkit 0.1.13__tar.gz → 0.1.15__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.15/.csharpierrc.json +6 -0
- unity_devkit-0.1.15/.github/actions/unity-container-prep/action.yml +37 -0
- unity_devkit-0.1.15/.github/workflows/ci.yml +51 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/.github/workflows/release.yml +5 -1
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/.github/workflows/unity-build.yml +15 -64
- unity_devkit-0.1.15/.github/workflows/unity-check.yml +58 -0
- unity_devkit-0.1.15/.github/workflows/unity-prepare.yml +90 -0
- unity_devkit-0.1.15/.gitignore +22 -0
- unity_devkit-0.1.15/AGENTS.md +57 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/PKG-INFO +2 -2
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/BuildConfig.cs +118 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/BuildConfig.cs.meta +2 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/ConfigureWindow.cs +238 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/ConfigureWindow.cs.meta +2 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/Outernet.PlayerBuild.Editor.asmdef +22 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/Outernet.PlayerBuild.Editor.asmdef.meta +7 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/PlayerBuild.cs +670 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/PlayerBuild.cs.meta +2 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/SerializableBuildReport.cs +76 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor/SerializableBuildReport.cs.meta +2 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/Editor.meta +8 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/package.json +13 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package/package.json.meta +7 -0
- unity_devkit-0.1.15/PlayerBuild/Assets/Package.meta +8 -0
- unity_devkit-0.1.15/PlayerBuild/Packages/manifest.json +8 -0
- unity_devkit-0.1.15/PlayerBuild/Packages/packages-lock.json +165 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/AudioManager.asset +20 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/ClusterInputManager.asset +6 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/DynamicsManager.asset +44 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/EditorBuildSettings.asset +9 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/EditorSettings.asset +50 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/GraphicsSettings.asset +66 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/InputManager.asset +296 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/MemorySettings.asset +35 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/MultiplayerManager.asset +7 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/NavMeshAreas.asset +93 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/Physics2DSettings.asset +56 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/PresetManager.asset +7 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/ProjectSettings.asset +785 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/ProjectVersion.txt +2 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/QualitySettings.asset +341 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/TagManager.asset +45 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/TimeManager.asset +14 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/UnityConnectSettings.asset +36 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/VFXManager.asset +19 -0
- unity_devkit-0.1.15/PlayerBuild/ProjectSettings/VersionControlSettings.asset +7 -0
- unity_devkit-0.1.15/README.md +51 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/pyproject.toml +4 -2
- unity_devkit-0.1.15/release-devkit.json +21 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/build_unity.py +10 -10
- unity_devkit-0.1.15/src/unity_devkit/check_unity.py +38 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/compile_unity.py +15 -5
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/install.py +4 -4
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/lock_unity.py +2 -2
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/matrix.py +20 -3
- unity_devkit-0.1.15/src/unity_devkit/projects.py +59 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/test_unity.py +3 -5
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/unity.py +10 -7
- unity_devkit-0.1.15/src/unity_devkit/versioning.py +37 -0
- unity_devkit-0.1.15/tests/test_latest_tag_version.py +19 -0
- unity_devkit-0.1.15/tests/test_matrix.py +60 -0
- unity_devkit-0.1.15/tests/test_projects.py +90 -0
- unity_devkit-0.1.15/tests/test_resolve.py +56 -0
- unity_devkit-0.1.15/tests/test_run_unity_batchmode.py +53 -0
- unity_devkit-0.1.15/tests/test_versioning.py +66 -0
- unity_devkit-0.1.15/unity-devkit.json +5 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/uv.lock +6 -6
- unity_devkit-0.1.13/.github/workflows/ci.yml +0 -26
- unity_devkit-0.1.13/.gitignore +0 -6
- unity_devkit-0.1.13/AGENTS.md +0 -51
- unity_devkit-0.1.13/README.md +0 -48
- unity_devkit-0.1.13/publish-config.json +0 -12
- 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.15}/.gitattributes +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/CLAUDE.md +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/LICENSE +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/NOTICE +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/ruff.base.toml +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/ruff.toml +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/__init__.py +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/license.py +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/license_restore.py +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/src/unity_devkit/py.typed +0 -0
- {unity_devkit-0.1.13 → unity_devkit-0.1.15}/tests/test_prepare.py +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Unity container prep
|
|
2
|
+
description: Wipe, checkout, uv setup, and license restore — the front-matter of every per-project Unity container job
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
cache-tag:
|
|
6
|
+
description: License cache tag to restore the activated license from
|
|
7
|
+
required: true
|
|
8
|
+
github-token:
|
|
9
|
+
required: true
|
|
10
|
+
unity-email:
|
|
11
|
+
required: true
|
|
12
|
+
unity-password:
|
|
13
|
+
required: true
|
|
14
|
+
unity-serial:
|
|
15
|
+
required: true
|
|
16
|
+
|
|
17
|
+
runs:
|
|
18
|
+
using: composite
|
|
19
|
+
steps:
|
|
20
|
+
- uses: astral-sh/setup-uv@v7
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
save-cache: "false"
|
|
24
|
+
|
|
25
|
+
- name: Sync dependencies
|
|
26
|
+
shell: bash
|
|
27
|
+
run: uv sync
|
|
28
|
+
|
|
29
|
+
- name: Activate Unity license
|
|
30
|
+
shell: bash
|
|
31
|
+
run: uv run --no-sync activate-unity-license --oras-push
|
|
32
|
+
env:
|
|
33
|
+
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
34
|
+
LICENSE_CACHE_TAG: ${{ inputs.cache-tag }}
|
|
35
|
+
UNITY_EMAIL: ${{ inputs.unity-email }}
|
|
36
|
+
UNITY_PASSWORD: ${{ inputs.unity-password }}
|
|
37
|
+
UNITY_SERIAL: ${{ inputs.unity-serial }}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
check:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v5
|
|
20
|
+
|
|
21
|
+
- uses: astral-sh/setup-uv@v7
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
|
|
25
|
+
- name: Preflight
|
|
26
|
+
run: uvx --from python-devkit==0.1.5 preflight-python
|
|
27
|
+
|
|
28
|
+
manifest:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v5
|
|
34
|
+
with:
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
|
|
37
|
+
- uses: astral-sh/setup-uv@v7
|
|
38
|
+
with:
|
|
39
|
+
enable-cache: true
|
|
40
|
+
|
|
41
|
+
- name: Publish plan dry run
|
|
42
|
+
run: uvx --from release-devkit==0.1.14 publish-stable --config release-devkit.json --dry-run
|
|
43
|
+
|
|
44
|
+
compile-check:
|
|
45
|
+
permissions:
|
|
46
|
+
contents: read
|
|
47
|
+
packages: write
|
|
48
|
+
uses: ./.github/workflows/unity-check.yml
|
|
49
|
+
with:
|
|
50
|
+
runner-labels: '["self-hosted","unity"]'
|
|
51
|
+
secrets: inherit
|
|
@@ -28,4 +28,8 @@ jobs:
|
|
|
28
28
|
- uses: astral-sh/setup-uv@v7
|
|
29
29
|
|
|
30
30
|
- name: Publish
|
|
31
|
-
|
|
31
|
+
env:
|
|
32
|
+
# npm decides in silence whether to attempt the OIDC exchange; without verbose a trusted-publisher
|
|
33
|
+
# mismatch surfaces as a bare ENEEDAUTH indistinguishable from a missing token
|
|
34
|
+
NPM_CONFIG_LOGLEVEL: verbose
|
|
35
|
+
run: uvx --from release-devkit==0.1.14 publish-stable --config release-devkit.json
|
|
@@ -23,64 +23,15 @@ env:
|
|
|
23
23
|
CACHE_REGISTRY: ghcr.io/${{ github.repository }}/cache
|
|
24
24
|
|
|
25
25
|
jobs:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
license-image: ${{ steps.generate.outputs.license-image }}
|
|
33
|
-
steps:
|
|
34
|
-
- uses: actions/checkout@v5
|
|
35
|
-
|
|
36
|
-
- uses: astral-sh/setup-uv@v7
|
|
37
|
-
with:
|
|
38
|
-
enable-cache: true
|
|
39
|
-
save-cache: "false"
|
|
40
|
-
|
|
41
|
-
- name: Sync dependencies
|
|
42
|
-
run: uv sync
|
|
43
|
-
|
|
44
|
-
- name: Generate build matrix
|
|
45
|
-
id: generate
|
|
46
|
-
run: uv run --no-sync unity-matrix >> "$GITHUB_OUTPUT"
|
|
47
|
-
|
|
48
|
-
activate-license:
|
|
49
|
-
needs: [matrix]
|
|
50
|
-
runs-on: ${{ fromJSON(inputs.runner-labels) }}
|
|
51
|
-
permissions:
|
|
52
|
-
contents: read
|
|
53
|
-
packages: write
|
|
54
|
-
outputs:
|
|
55
|
-
cache-tag: ${{ steps.tag.outputs.value }}
|
|
56
|
-
container:
|
|
57
|
-
image: ${{ needs.matrix.outputs.license-image }}
|
|
58
|
-
steps:
|
|
59
|
-
- uses: actions/checkout@v5
|
|
60
|
-
|
|
61
|
-
- uses: astral-sh/setup-uv@v7
|
|
62
|
-
with:
|
|
63
|
-
enable-cache: true
|
|
64
|
-
save-cache: "false"
|
|
65
|
-
|
|
66
|
-
- name: Sync dependencies
|
|
67
|
-
run: uv sync
|
|
68
|
-
|
|
69
|
-
- name: Compute license cache tag
|
|
70
|
-
id: tag
|
|
71
|
-
run: echo "value=$(uv run --no-sync unity-license-tag)" >> "$GITHUB_OUTPUT"
|
|
72
|
-
|
|
73
|
-
- name: Activate Unity license
|
|
74
|
-
run: uv run --no-sync activate-unity-license --oras-push
|
|
75
|
-
env:
|
|
76
|
-
GITHUB_TOKEN: ${{ github.token }}
|
|
77
|
-
LICENSE_CACHE_TAG: ${{ steps.tag.outputs.value }}
|
|
78
|
-
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
|
79
|
-
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
|
80
|
-
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
|
26
|
+
prepare:
|
|
27
|
+
uses: ./.github/workflows/unity-prepare.yml
|
|
28
|
+
with:
|
|
29
|
+
runner-labels: ${{ inputs.runner-labels }}
|
|
30
|
+
matrix-command: uv run --no-sync unity-matrix
|
|
31
|
+
secrets: inherit
|
|
81
32
|
|
|
82
33
|
build-unity:
|
|
83
|
-
needs: [
|
|
34
|
+
needs: [prepare]
|
|
84
35
|
name: ${{ matrix.project-name }} (${{ matrix.platform }})
|
|
85
36
|
runs-on: ${{ fromJSON(inputs.runner-labels) }}
|
|
86
37
|
permissions:
|
|
@@ -90,7 +41,7 @@ jobs:
|
|
|
90
41
|
image: ${{ matrix.editor-image }}
|
|
91
42
|
strategy:
|
|
92
43
|
fail-fast: false
|
|
93
|
-
matrix: ${{ fromJson(needs.
|
|
44
|
+
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
|
|
94
45
|
steps:
|
|
95
46
|
- name: Wipe workspace
|
|
96
47
|
run: |
|
|
@@ -99,13 +50,13 @@ jobs:
|
|
|
99
50
|
|
|
100
51
|
- uses: actions/checkout@v5
|
|
101
52
|
|
|
102
|
-
- uses:
|
|
53
|
+
- uses: ./.github/actions/unity-container-prep
|
|
103
54
|
with:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
55
|
+
cache-tag: ${{ needs.prepare.outputs.cache-tag }}
|
|
56
|
+
github-token: ${{ github.token }}
|
|
57
|
+
unity-email: ${{ secrets.UNITY_EMAIL }}
|
|
58
|
+
unity-password: ${{ secrets.UNITY_PASSWORD }}
|
|
59
|
+
unity-serial: ${{ secrets.UNITY_SERIAL }}
|
|
109
60
|
|
|
110
61
|
- name: Cache UPM packages
|
|
111
62
|
uses: actions/cache@v5
|
|
@@ -116,7 +67,7 @@ jobs:
|
|
|
116
67
|
- name: Build
|
|
117
68
|
env:
|
|
118
69
|
GITHUB_TOKEN: ${{ github.token }}
|
|
119
|
-
LICENSE_CACHE_TAG: ${{ needs.
|
|
70
|
+
LICENSE_CACHE_TAG: ${{ needs.prepare.outputs.cache-tag }}
|
|
120
71
|
BUILD_ENV: ${{ inputs.build-env }}
|
|
121
72
|
run: |
|
|
122
73
|
uv run --no-sync build-unity \
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Unity Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
runner-labels:
|
|
7
|
+
description: JSON-encoded runs-on value (string or array) for the license/check jobs
|
|
8
|
+
type: string
|
|
9
|
+
default: '"ubuntu-latest"'
|
|
10
|
+
secrets:
|
|
11
|
+
UNITY_EMAIL:
|
|
12
|
+
required: true
|
|
13
|
+
UNITY_PASSWORD:
|
|
14
|
+
required: true
|
|
15
|
+
UNITY_SERIAL:
|
|
16
|
+
required: true
|
|
17
|
+
|
|
18
|
+
env:
|
|
19
|
+
CACHE_REGISTRY: ghcr.io/${{ github.repository }}/cache
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
prepare:
|
|
23
|
+
uses: ./.github/workflows/unity-prepare.yml
|
|
24
|
+
with:
|
|
25
|
+
runner-labels: ${{ inputs.runner-labels }}
|
|
26
|
+
matrix-command: uv run --no-sync unity-check-matrix
|
|
27
|
+
secrets: inherit
|
|
28
|
+
|
|
29
|
+
check-unity:
|
|
30
|
+
needs: [prepare]
|
|
31
|
+
name: ${{ matrix.project-name }} (check)
|
|
32
|
+
runs-on: ${{ fromJSON(inputs.runner-labels) }}
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
packages: write
|
|
36
|
+
container:
|
|
37
|
+
image: ${{ matrix.editor-image }}
|
|
38
|
+
strategy:
|
|
39
|
+
fail-fast: false
|
|
40
|
+
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
|
|
41
|
+
steps:
|
|
42
|
+
- name: Wipe workspace
|
|
43
|
+
run: |
|
|
44
|
+
find "$GITHUB_WORKSPACE" -mindepth 1 -delete
|
|
45
|
+
rm -rf ~/.cache/Unity
|
|
46
|
+
|
|
47
|
+
- uses: actions/checkout@v5
|
|
48
|
+
|
|
49
|
+
- uses: ./.github/actions/unity-container-prep
|
|
50
|
+
with:
|
|
51
|
+
cache-tag: ${{ needs.prepare.outputs.cache-tag }}
|
|
52
|
+
github-token: ${{ github.token }}
|
|
53
|
+
unity-email: ${{ secrets.UNITY_EMAIL }}
|
|
54
|
+
unity-password: ${{ secrets.UNITY_PASSWORD }}
|
|
55
|
+
unity-serial: ${{ secrets.UNITY_SERIAL }}
|
|
56
|
+
|
|
57
|
+
- name: Compile check
|
|
58
|
+
run: uv run --no-sync check-unity --project ${{ matrix.project-name }}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Unity Prepare
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
runner-labels:
|
|
7
|
+
description: JSON-encoded runs-on value (string or array) for the license/matrix jobs
|
|
8
|
+
type: string
|
|
9
|
+
default: '"ubuntu-latest"'
|
|
10
|
+
matrix-command:
|
|
11
|
+
description: uv run command whose stdout carries matrix=<json> and license-image=<tag> lines
|
|
12
|
+
type: string
|
|
13
|
+
required: true
|
|
14
|
+
outputs:
|
|
15
|
+
matrix:
|
|
16
|
+
description: The include matrix emitted by matrix-command
|
|
17
|
+
value: ${{ jobs.matrix.outputs.matrix }}
|
|
18
|
+
license-image:
|
|
19
|
+
description: The editor image for license activation
|
|
20
|
+
value: ${{ jobs.matrix.outputs.license-image }}
|
|
21
|
+
cache-tag:
|
|
22
|
+
description: The license cache tag pushed by activation
|
|
23
|
+
value: ${{ jobs.activate-license.outputs.cache-tag }}
|
|
24
|
+
secrets:
|
|
25
|
+
UNITY_EMAIL:
|
|
26
|
+
required: true
|
|
27
|
+
UNITY_PASSWORD:
|
|
28
|
+
required: true
|
|
29
|
+
UNITY_SERIAL:
|
|
30
|
+
required: true
|
|
31
|
+
|
|
32
|
+
env:
|
|
33
|
+
CACHE_REGISTRY: ghcr.io/${{ github.repository }}/cache
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
matrix:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
permissions:
|
|
39
|
+
contents: read
|
|
40
|
+
outputs:
|
|
41
|
+
matrix: ${{ steps.generate.outputs.matrix }}
|
|
42
|
+
license-image: ${{ steps.generate.outputs.license-image }}
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v5
|
|
45
|
+
|
|
46
|
+
- uses: astral-sh/setup-uv@v7
|
|
47
|
+
with:
|
|
48
|
+
enable-cache: true
|
|
49
|
+
save-cache: "false"
|
|
50
|
+
|
|
51
|
+
- name: Sync dependencies
|
|
52
|
+
run: uv sync
|
|
53
|
+
|
|
54
|
+
- name: Generate matrix
|
|
55
|
+
id: generate
|
|
56
|
+
run: ${{ inputs.matrix-command }} >> "$GITHUB_OUTPUT"
|
|
57
|
+
|
|
58
|
+
activate-license:
|
|
59
|
+
needs: [matrix]
|
|
60
|
+
runs-on: ${{ fromJSON(inputs.runner-labels) }}
|
|
61
|
+
permissions:
|
|
62
|
+
contents: read
|
|
63
|
+
packages: write
|
|
64
|
+
outputs:
|
|
65
|
+
cache-tag: ${{ steps.tag.outputs.value }}
|
|
66
|
+
container:
|
|
67
|
+
image: ${{ needs.matrix.outputs.license-image }}
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v5
|
|
70
|
+
|
|
71
|
+
- uses: astral-sh/setup-uv@v7
|
|
72
|
+
with:
|
|
73
|
+
enable-cache: true
|
|
74
|
+
save-cache: "false"
|
|
75
|
+
|
|
76
|
+
- name: Sync dependencies
|
|
77
|
+
run: uv sync
|
|
78
|
+
|
|
79
|
+
- name: Compute license cache tag
|
|
80
|
+
id: tag
|
|
81
|
+
run: echo "value=$(uv run --no-sync unity-license-tag)" >> "$GITHUB_OUTPUT"
|
|
82
|
+
|
|
83
|
+
- name: Activate Unity license
|
|
84
|
+
run: uv run --no-sync activate-unity-license --oras-push
|
|
85
|
+
env:
|
|
86
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
87
|
+
LICENSE_CACHE_TAG: ${{ steps.tag.outputs.value }}
|
|
88
|
+
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
|
89
|
+
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
|
90
|
+
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.ruff_cache/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
bookmark.md
|
|
7
|
+
|
|
8
|
+
# PlayerBuild Unity harness
|
|
9
|
+
PlayerBuild/*.csproj
|
|
10
|
+
PlayerBuild/*.sln
|
|
11
|
+
PlayerBuild/Assets/Packages/
|
|
12
|
+
PlayerBuild/Assets/_LocalWorkspace
|
|
13
|
+
PlayerBuild/Assets/_LocalWorkspace.meta
|
|
14
|
+
PlayerBuild/Build/
|
|
15
|
+
PlayerBuild/Build.meta
|
|
16
|
+
PlayerBuild/Builds/
|
|
17
|
+
PlayerBuild/Library/
|
|
18
|
+
PlayerBuild/Logs/
|
|
19
|
+
PlayerBuild/Temp/
|
|
20
|
+
PlayerBuild/UserSettings/
|
|
21
|
+
PlayerBuild/obj/
|
|
22
|
+
PlayerBuild/.utmp/
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# unity-devkit
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
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 two reusable GitHub Actions workflows hosted in this repo's `.github/workflows/`, both invoking only this package's entry points inside the caller's checkout and both consumed cross-repo pinned to a pushed SHA with `secrets: inherit`: `unity-build.yml` (player builds) and `unity-check.yml` (the same machinery reduced to a compile gate — `check-unity` per cataloged project, path-only entries included). Their shared scaffold lives in `unity-prepare.yml` — a matrix job parameterized by the matrix verb, plus the ORAS-backed license activation — and the wipe/checkout/uv/license-restore front-matter of every per-project container job lives in the `.github/actions/unity-container-prep` composite action; consumers never call those two directly. Both doors need `UNITY_EMAIL`/`UNITY_PASSWORD`/`UNITY_SERIAL` available to the calling repo, and every Unity invocation runs in a `unityci/editor` container image composed from the project's `ProjectVersion.txt` (the runners supply Docker, not an installed editor).
|
|
6
|
+
|
|
7
|
+
This repo also develops and publishes the **`org.outernet.playerbuild`** UPM editor package (npm) — the inside-of-Unity half of app builds: platform-fact application from a C# table, the batchmode build entry, the editor configure window, and build-time environment materialization. Its source lives in `PlayerBuild/Assets/Package/`, developed inside the `PlayerBuild/` Unity harness project (catalog entry of the same name); CI's `compile-check` job is its compile gate. Both folded in from the retired `unitybuild` repo (2026-09-27); that repo's docs and history remain the record of the package's design.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
## Release flow
|
|
12
|
+
|
|
13
|
+
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 same publish also covers the npm package: `release-devkit.json` declares two packages — `unity-devkit` (PyPI, path `.`, `unity-devkit-v*` ledger) and `playerbuild` (npm `org.outernet.playerbuild`, path `PlayerBuild/Assets/Package`, `playerbuild-v*` ledger), each gated by its own path-diff. The npm trusted publisher is bound to this repo's `release.yml` (one publisher per package, single workflow filename — never rename it), and the publish step sets `NPM_CONFIG_LOGLEVEL: verbose` because npm decides in silence whether to attempt the OIDC exchange. The committed `pyproject.toml` version is permanently the `0.0.0.dev0` sentinel; the tags are the version ledger (declared `major_minor` line in `release-devkit.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.
|
|
14
|
+
|
|
15
|
+
## Shape
|
|
16
|
+
|
|
17
|
+
One flat module per concern under `src/unity_devkit/`:
|
|
18
|
+
|
|
19
|
+
| `uv run` command | Module | Notes |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `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/`. `--stamp-version` stamps the tag-ledger version into `ProjectSettings.asset` before the run. |
|
|
22
|
+
| `check-unity` | `check_unity.py` | Compile gate: batchmode open+quit (`-nographics`, no executeMethod) failing on non-zero exit or any `error CS` in the log — Unity exits 0 on a plain open+quit despite script compile errors, so the log scan is the authoritative signal. Path-only (needs no `builds`); the gate for package/harness repos that own editor code but build no players. Two optional flags turn it into a build-free method driver: `--execute-method Class.Method` runs a static method after load (one editor session per invocation — the vehicle for committed-generated-asset regeneration, where each pass needs a fresh session) and `--build-target <target>` forces the startup build target for sessions that must open on a non-default platform. Player builds must not ride this verb (`-nographics` + `-quit` cannot build players); use `compile-unity`. |
|
|
23
|
+
| `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`). |
|
|
24
|
+
| `lock-unity` | `lock_unity.py` | Lock Unity package versions for reproducible builds. |
|
|
25
|
+
| `test-unity` | `test_unity.py` | Run Unity editmode / playmode tests. |
|
|
26
|
+
| `activate-unity-license` | `license.py` | Activate the Unity Editor license (locally or with `--oras-push` for the CI cache). |
|
|
27
|
+
| `unity-license-tag` | `license_restore.py` | Print the license cache tag (CI pins it via `LICENSE_CACHE_TAG`). |
|
|
28
|
+
| `unity-matrix` | `matrix.py` | Emit the CI build matrix (CI-only). |
|
|
29
|
+
| `unity-check-matrix` | `matrix.py` | Emit the CI compile-check matrix (CI-only): one entry per cataloged project — path-only entries included, no `builds` needed — each carrying a `linux-il2cpp` editor image composed from that project's `ProjectVersion.txt`, plus the `license-image` line. Consumed by the hosted `unity-check.yml`. |
|
|
30
|
+
| `build-unity` | `build_unity.py` | CI build with library cache restore/save and ProjectSettings version stamping (CI-only; assumes `GITHUB_WORKSPACE`, OCI registry, runner environment). |
|
|
31
|
+
|
|
32
|
+
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), `versioning.py` (tag-ledger version computation and the `ProjectSettings.asset` stamping both build verbs share). 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.
|
|
33
|
+
|
|
34
|
+
The `PlayerBuild/` harness is this repo's one cataloged Unity project: a minimal Unity `6000.0.66f1` host (no scenes, no XR settings asset, no `build-config.json` — package entry paths fail loudly there by design) whose only job is compiling and developing `Assets/Package/`. C# style is the owner's ruleset recorded in the unitybuild record; CSharpier (`.csharpierrc.json` at the repo root, 120 cols) formats `PlayerBuild/Assets/Package/**/*.cs` — run `csharpier format` before committing C# changes. The package's architecture and semantics doc lives in its own shipped files plus the unitybuild record; the package convention summary: namespace `Outernet` flat, no unit tests ever (the compile gate is the test surface), `.meta` files committed alongside.
|
|
35
|
+
|
|
36
|
+
## Constraints
|
|
37
|
+
|
|
38
|
+
**Every Unity invocation goes through `unity.run_unity_batchmode`, which does not trust the editor's exit code.** Unity exits 0 while reporting fatal package-manager errors (unresolvable dependencies, invalid package.json versions) only in the editor log — a resolver failure therefore surfaces as a stale `packages-lock.json` and nothing else. The runner streams the log live through `tee` into a captured file and afterwards scans it for `QUIET_FAILURE_SIGNATURES`; a match fails the invocation with the matched log block regardless of exit code. New Unity verbs must call the runner rather than invoking the editor directly, and newly discovered silent-failure log lines get added to the signature tuple. Verb-local signatures ride the same scan via `extra_failure_signatures`: `check-unity` passes `("error CS",)` because a plain open+quit exits 0 despite script compile errors — the signature stays verb-local because `lock-unity` intentionally tolerates them. Exit-code strictness is per-verb: build/test verbs fail on non-zero (`strict_exit=True`, the default), while `lock-unity` passes `strict_exit=False` — it judges resolution, not compilation, so a project whose scripts fail to compile (resolution already done, lock written) produces a warning, not a failure. `-runTests` invocations pass `auto_quit=False` (the test runner owns exit timing).
|
|
39
|
+
|
|
40
|
+
**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.
|
|
41
|
+
|
|
42
|
+
**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`, `check-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 build manifests and the structural marker walk are gone from the verbs; the pruned walk (`projects.directories_containing`) remains as cross-repo API.
|
|
43
|
+
|
|
44
|
+
**`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.
|
|
45
|
+
|
|
46
|
+
**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.
|
|
47
|
+
|
|
48
|
+
**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`.
|
|
49
|
+
|
|
50
|
+
**Version stamping is opt-in via `tag_prefix` and writes `ProjectSettings.asset` directly.** When the project's catalog entry declares `tag_prefix`, `build-unity` stamps `bundleVersion` and `AndroidBundleVersionCode` (two scalar line edits in `ProjectSettings/ProjectSettings.asset` — no Unity API, no executeMethod contract) before batchmode; `compile-unity` does the same when passed `--stamp-version`. 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; the Android `bundleVersionCode` is the run number. Projects without the field build unversioned. Precondition: Unity not running on the project. 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.
|
|
51
|
+
|
|
52
|
+
**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.
|
|
53
|
+
|
|
54
|
+
## See also
|
|
55
|
+
|
|
56
|
+
- [`bashrun`](https://github.com/outernet-foundation/bashrun) — the shell-exec helpers this package uses everywhere (`bash`, `bash_output`, `bash_check`, `bash_handoff`).
|
|
57
|
+
- `README.md` — human-facing setup, command catalog, and consumer install snippet.
|
|
@@ -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.15
|
|
4
|
+
Summary: Unity build, license, and CI tooling
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
License-File: NOTICE
|
|
7
7
|
Requires-Python: >=3.13
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Runtime.Serialization;
|
|
4
|
+
using Newtonsoft.Json;
|
|
5
|
+
using Newtonsoft.Json.Converters;
|
|
6
|
+
using UnityEditor.Build;
|
|
7
|
+
|
|
8
|
+
namespace Outernet
|
|
9
|
+
{
|
|
10
|
+
[JsonConverter(typeof(StringEnumConverter))]
|
|
11
|
+
public enum EnvironmentFieldType
|
|
12
|
+
{
|
|
13
|
+
[EnumMember(Value = "string")]
|
|
14
|
+
String,
|
|
15
|
+
|
|
16
|
+
[EnumMember(Value = "boolean")]
|
|
17
|
+
Boolean,
|
|
18
|
+
|
|
19
|
+
[EnumMember(Value = "integer")]
|
|
20
|
+
Integer,
|
|
21
|
+
|
|
22
|
+
[EnumMember(Value = "enum")]
|
|
23
|
+
Enum,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public sealed class EnvironmentConfig
|
|
27
|
+
{
|
|
28
|
+
[JsonProperty("target", Required = Required.Always)]
|
|
29
|
+
public string Target { get; set; } = "";
|
|
30
|
+
|
|
31
|
+
[JsonProperty("default_preset")]
|
|
32
|
+
public string DefaultPreset { get; set; } = "";
|
|
33
|
+
|
|
34
|
+
[JsonProperty("presets", Required = Required.DisallowNull)]
|
|
35
|
+
public Dictionary<string, string> Presets { get; set; } = new();
|
|
36
|
+
|
|
37
|
+
[JsonProperty("fields")]
|
|
38
|
+
public Dictionary<string, EnvironmentField> Fields { get; set; } = new();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public sealed class EnvironmentField
|
|
42
|
+
{
|
|
43
|
+
[JsonProperty("name")]
|
|
44
|
+
public string Name { get; set; } = "";
|
|
45
|
+
|
|
46
|
+
[JsonProperty("path", Required = Required.Always)]
|
|
47
|
+
public string Path { get; set; } = "";
|
|
48
|
+
|
|
49
|
+
[JsonProperty("type", Required = Required.Always)]
|
|
50
|
+
public EnvironmentFieldType Type { get; set; }
|
|
51
|
+
|
|
52
|
+
[JsonProperty("values")]
|
|
53
|
+
public List<string> Values { get; set; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public sealed class PlatformOverrides
|
|
57
|
+
{
|
|
58
|
+
[JsonProperty("render_pipeline")]
|
|
59
|
+
public string RenderPipeline { get; set; } = "";
|
|
60
|
+
|
|
61
|
+
[JsonProperty("additional_defines")]
|
|
62
|
+
public List<string> AdditionalDefines { get; set; } = new();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public sealed class BuildConfig
|
|
66
|
+
{
|
|
67
|
+
[JsonProperty("environment_config", Required = Required.DisallowNull)]
|
|
68
|
+
public EnvironmentConfig EnvironmentConfig { get; set; } = new();
|
|
69
|
+
|
|
70
|
+
[JsonProperty("platforms", Required = Required.DisallowNull)]
|
|
71
|
+
public Dictionary<string, PlatformOverrides> Platforms { get; set; } = new();
|
|
72
|
+
|
|
73
|
+
[OnDeserialized]
|
|
74
|
+
private void Validate(StreamingContext context)
|
|
75
|
+
{
|
|
76
|
+
if (
|
|
77
|
+
!string.IsNullOrEmpty(EnvironmentConfig.DefaultPreset)
|
|
78
|
+
&& !EnvironmentConfig.Presets.ContainsKey(EnvironmentConfig.DefaultPreset)
|
|
79
|
+
)
|
|
80
|
+
{
|
|
81
|
+
throw new BuildFailedException(
|
|
82
|
+
$"default_preset '{EnvironmentConfig.DefaultPreset}' is not a declared preset (declared: {string.Join(", ", EnvironmentConfig.Presets.Keys)})"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
foreach (KeyValuePair<string, string> preset in EnvironmentConfig.Presets)
|
|
87
|
+
{
|
|
88
|
+
if (!preset.Value.StartsWith("Assets/", StringComparison.Ordinal))
|
|
89
|
+
{
|
|
90
|
+
throw new BuildFailedException(
|
|
91
|
+
$"build-config.json field 'presets[{preset.Key}]' must be a project path starting with Assets/ — got '{preset.Value}'"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
foreach (KeyValuePair<string, EnvironmentField> entry in EnvironmentConfig.Fields)
|
|
97
|
+
{
|
|
98
|
+
if (entry.Value.Type == EnvironmentFieldType.Enum && entry.Value.Values is not { Count: > 0 })
|
|
99
|
+
{
|
|
100
|
+
throw new BuildFailedException($"fields[{entry.Key}] is an enum without a values list");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
foreach (KeyValuePair<string, PlatformOverrides> entry in Platforms)
|
|
105
|
+
{
|
|
106
|
+
if (
|
|
107
|
+
entry.Value.RenderPipeline.Length > 0
|
|
108
|
+
&& !entry.Value.RenderPipeline.StartsWith("Assets/", StringComparison.Ordinal)
|
|
109
|
+
)
|
|
110
|
+
{
|
|
111
|
+
throw new BuildFailedException(
|
|
112
|
+
$"build-config.json field 'platforms[{entry.Key}].render_pipeline' must be a project path starting with Assets/ — got '{entry.Value.RenderPipeline}'"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|