unity-devkit 0.1.16__tar.gz → 0.1.17__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 (24) hide show
  1. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/PKG-INFO +1 -1
  2. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/pyproject.toml +1 -1
  3. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/dispatch_workflow_generator.py +31 -39
  4. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/tests/test_dispatch_workflow_generator.py +61 -11
  5. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/.gitignore +0 -0
  6. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/__init__.py +0 -0
  7. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/build_unity.py +0 -0
  8. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/ci_build_unity.py +0 -0
  9. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/compile_check_unity.py +0 -0
  10. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/install.py +0 -0
  11. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/license.py +0 -0
  12. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/license_restore.py +0 -0
  13. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/lock_unity.py +0 -0
  14. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/matrix.py +0 -0
  15. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/player_build.py +0 -0
  16. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/projects.py +0 -0
  17. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/py.typed +0 -0
  18. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/src/unity_devkit/test_unity.py +0 -0
  19. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/tests/fixtures/environment-dump.json +0 -0
  20. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/tests/test_build_player.py +0 -0
  21. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/tests/test_matrix.py +0 -0
  22. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/tests/test_prepare.py +0 -0
  23. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/tests/test_projects.py +0 -0
  24. {unity_devkit-0.1.16 → unity_devkit-0.1.17}/tests/test_run_unity_batchmode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unity-devkit
3
- Version: 0.1.16
3
+ Version: 0.1.17
4
4
  Summary: Unity build, license, and CI tooling
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: bashrun>=0.1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unity-devkit"
3
- version = "0.1.16"
3
+ version = "0.1.17"
4
4
  description = "Unity build, license, and CI tooling"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
@@ -1,4 +1,5 @@
1
1
  import json
2
+ from collections import Counter
2
3
  from pathlib import Path
3
4
  from typing import Annotated
4
5
 
@@ -13,6 +14,7 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
13
14
 
14
15
  DUMP_EXECUTE_METHOD = "Outernet.PlayerBuild.DumpEnvironment"
15
16
  DISPATCH_INPUT_CAP = 25
17
+ RESERVED_INPUT_IDS = frozenset({"environment-preset", "development"})
16
18
  NUMERIC_FIELD_TYPES = {
17
19
  "Byte",
18
20
  "SByte",
@@ -124,19 +126,26 @@ def extract_environment_dump(log_path: Path) -> EnvironmentDump:
124
126
 
125
127
  def render_dispatch_workflow(dump: EnvironmentDump, *, project: str, output: str, build_workflow: str) -> str:
126
128
  enums_by_name = {environment_enum.name: environment_enum for environment_enum in dump.enums}
127
- input_blocks: list[tuple[str, str, list[str], str | None]] = [
129
+ emitted_fields = [field for field in dump.fields if field.path != dump.mode_field]
130
+ leaf_counts = Counter(field.path.rsplit(".", 1)[-1] for field in emitted_fields)
131
+ input_ids: dict[str, str] = {}
132
+ for field in emitted_fields:
133
+ leaf = field.path.rsplit(".", 1)[-1]
134
+ input_ids[field.path] = (
135
+ leaf if leaf_counts[leaf] == 1 and leaf not in RESERVED_INPUT_IDS else field.path.replace(".", "-")
136
+ )
137
+ default_preset = next(iter(dump.presets), "")
138
+ input_blocks: list[tuple[str, str | None, list[str], str | None]] = [
128
139
  (
129
140
  "environment-preset",
130
- "Environment preset (Presets key) — empty leaves the workspace untouched",
141
+ "Preset to apply — blank leaves the workspace untouched; leave fields empty to keep the preset's values",
131
142
  ["type: choice", "options:", *indent_lines([*(f'- "{preset}"' for preset in ("", *dump.presets))], 2)],
132
- 'default: ""',
143
+ f'default: "{default_preset}"',
133
144
  ),
134
- ("development", "Development build column", ["type: boolean"], "default: false"),
145
+ ("development", "Development build (debug symbols)", ["type: boolean"], "default: false"),
135
146
  ]
136
- for field in dump.fields:
137
- if field.path == dump.mode_field:
138
- continue
139
- input_blocks.append(field_input_block(field, enums_by_name))
147
+ for field in emitted_fields:
148
+ input_blocks.append(field_input_block(field, input_ids[field.path], enums_by_name))
140
149
 
141
150
  input_count = len(input_blocks)
142
151
  if input_count > DISPATCH_INPUT_CAP:
@@ -145,7 +154,7 @@ def render_dispatch_workflow(dump: EnvironmentDump, *, project: str, output: str
145
154
  "Shrink the env class or move configuration out of it."
146
155
  )
147
156
 
148
- transport_lines = [field_transport_line(field) for field in dump.fields if field.path != dump.mode_field]
157
+ transport_lines = [field_transport_line(field, input_ids[field.path]) for field in emitted_fields]
149
158
  header = (
150
159
  f"# Generated by '{regen_command(project, output, build_workflow)}' from the environment class "
151
160
  f"{dump.class_name} —"
@@ -182,32 +191,23 @@ def regen_command(project: str, output: str, build_workflow: str) -> str:
182
191
 
183
192
 
184
193
  def field_input_block(
185
- field: EnvironmentFieldDump, enums_by_name: dict[str, EnvironmentEnum]
186
- ) -> tuple[str, str, list[str], str | None]:
187
- input_id = field.path.replace(".", "-")
194
+ field: EnvironmentFieldDump, input_id: str, enums_by_name: dict[str, EnvironmentEnum]
195
+ ) -> tuple[str, str | None, list[str], str | None]:
188
196
  if field.field_type == "Boolean":
189
- return (
190
- input_id,
191
- f"environment field {field.path} (bool) — checked overrides the preset value",
192
- ["type: boolean"],
193
- "default: false",
194
- )
197
+ return (input_id, None, ["type: boolean"], "default: false")
195
198
 
196
199
  if field.field_type in enums_by_name:
197
200
  environment_enum = enums_by_name[field.field_type]
198
201
  if environment_enum.flags:
199
202
  return (
200
203
  input_id,
201
- (
202
- f"environment field {field.path} (flags: {', '.join(environment_enum.values)}) — "
203
- "comma-separated names, empty keeps the preset value"
204
- ),
204
+ f"comma-separated: {', '.join(environment_enum.values)}",
205
205
  ["type: string"],
206
206
  'default: ""',
207
207
  )
208
208
  return (
209
209
  input_id,
210
- f"environment field {field.path} ({environment_enum.name}) — empty keeps the preset value",
210
+ None,
211
211
  [
212
212
  "type: choice",
213
213
  "options:",
@@ -217,20 +217,10 @@ def field_input_block(
217
217
  )
218
218
 
219
219
  if field.field_type == "String":
220
- return (
221
- input_id,
222
- f"environment field {field.path} (string) — empty keeps the preset value",
223
- ["type: string"],
224
- 'default: ""',
225
- )
220
+ return (input_id, None, ["type: string"], 'default: ""')
226
221
 
227
222
  if field.field_type in NUMERIC_FIELD_TYPES:
228
- return (
229
- input_id,
230
- f"environment field {field.path} ({field.field_type}) — empty keeps the preset value",
231
- ["type: number"],
232
- None,
233
- )
223
+ return (input_id, None, ["type: number"], None)
234
224
 
235
225
  raise SystemExit(
236
226
  f"Environment field '{field.path}' has type '{field.field_type}' — no dispatch input type maps to it "
@@ -238,8 +228,7 @@ def field_input_block(
238
228
  )
239
229
 
240
230
 
241
- def field_transport_line(field: EnvironmentFieldDump) -> str:
242
- input_id = field.path.replace(".", "-")
231
+ def field_transport_line(field: EnvironmentFieldDump, input_id: str) -> str:
243
232
  guard = "" if field.field_type == "Boolean" else " != ''"
244
233
  return (
245
234
  "${{ inputs['"
@@ -254,9 +243,12 @@ def field_transport_line(field: EnvironmentFieldDump) -> str:
254
243
  )
255
244
 
256
245
 
257
- def build_input_block(input_block: tuple[str, str, list[str], str | None]) -> list[str]:
246
+ def build_input_block(input_block: tuple[str, str | None, list[str], str | None]) -> list[str]:
258
247
  input_id, description, type_lines, default_line = input_block
259
- lines = [f"{input_id}:", f' description: "{description}"', *[f" {line}" for line in type_lines]]
248
+ lines = [f"{input_id}:"]
249
+ if description is not None:
250
+ lines.append(f' description: "{description}"')
251
+ lines.extend(f" {line}" for line in type_lines)
260
252
  if default_line is not None:
261
253
  lines.append(f" {default_line}")
262
254
  return lines
@@ -38,22 +38,72 @@ def test_render_modes_are_deduped_into_the_preset_choice() -> None:
38
38
  assert 'options:\n - ""\n - "Airgapped"' in workflow
39
39
 
40
40
 
41
- def test_render_maps_field_types_to_dispatch_inputs() -> None:
41
+ def test_render_defaults_the_preset_choice_to_the_first_map_key() -> None:
42
+ document = yaml.safe_load(render_fixture_workflow().replace("${{", "__EXPR_OPEN__").replace("}}", "__EXPR_CLOSE__"))
43
+
44
+ trigger = document.get("on", document[True])
45
+ preset = trigger["workflow_dispatch"]["inputs"]["environment-preset"]
46
+ assert preset["options"] == ["", "Airgapped"]
47
+ assert preset["default"] == "Airgapped"
48
+
49
+
50
+ def test_render_states_the_override_law_once_on_the_preset_input() -> None:
51
+ workflow = render_fixture_workflow()
52
+
53
+ assert "environment field" not in workflow
54
+ assert "empty keeps the preset value" not in workflow
55
+ assert "checked overrides the preset value" not in workflow
56
+ assert workflow.count("description:") == 3
57
+
58
+
59
+ def test_render_maps_field_types_to_dispatch_inputs_without_descriptions() -> None:
60
+ workflow = render_fixture_workflow()
61
+
62
+ assert "\n username:\n type: string" in workflow
63
+ assert "\n verboseLogging:\n type: boolean" in workflow
64
+ assert "\n pollIntervalSeconds:\n type: number" in workflow
65
+
66
+
67
+ def test_render_uses_leaf_names_as_input_ids_when_unique() -> None:
42
68
  workflow = render_fixture_workflow()
43
69
 
44
- assert 'username:\n description: "environment field username (string)' in workflow
45
- assert 'verboseLogging:\n description: "environment field verboseLogging (bool)' in workflow
46
- assert "type: number" in workflow
47
- assert "localConfig-apiUrl" in workflow
48
- assert "inputs['localConfig-apiUrl']" in workflow
70
+ assert "\n apiUrl:" in workflow
71
+ assert "\n portNumber:" in workflow
72
+ assert "localConfig-apiUrl" not in workflow
73
+ assert "${{ inputs['apiUrl'] != '' && format('localConfig.apiUrl={0}', inputs['apiUrl']) || '' }}" in workflow
74
+
75
+
76
+ def test_render_falls_back_to_dashed_paths_on_leaf_collision() -> None:
77
+ dump = load_fixture_dump()
78
+ dump.fields.append(EnvironmentFieldDump(path="remoteConfig.apiUrl", field_type="String"))
79
+
80
+ workflow = render_dispatch_workflow(
81
+ dump, project="PlayerBuild", output=".github/workflows/build.yml", build_workflow=PINNED_BUILD_WORKFLOW
82
+ )
83
+
84
+ assert "\n apiUrl:" not in workflow
85
+ assert "\n localConfig-apiUrl:" in workflow
86
+ assert "\n remoteConfig-apiUrl:" in workflow
87
+
88
+
89
+ def test_render_falls_back_to_dashed_paths_on_control_id_collision() -> None:
90
+ dump = load_fixture_dump()
91
+ dump.fields.append(EnvironmentFieldDump(path="misc.development", field_type="Boolean"))
92
+
93
+ workflow = render_dispatch_workflow(
94
+ dump, project="PlayerBuild", output=".github/workflows/build.yml", build_workflow=PINNED_BUILD_WORKFLOW
95
+ )
96
+
97
+ assert "\n misc-development:" in workflow
98
+ assert workflow.split("jobs:")[0].count("\n development:") == 1
49
99
 
50
100
 
51
101
  def test_render_flags_enum_is_text_not_choice() -> None:
52
102
  workflow = render_fixture_workflow()
53
103
 
54
- assert "comma-separated names" in workflow
55
- capabilities_choice = "capabilities:\n description:"
56
- assert capabilities_choice in workflow
104
+ assert 'description: "comma-separated: None, Capture, Stream, Analyze"' in workflow
105
+ assert "\n capabilities:\n description:" in workflow
106
+ assert "\n capabilities:\n type: string" not in workflow
57
107
  assert "type: choice" in workflow
58
108
 
59
109
 
@@ -106,8 +156,8 @@ def test_rendered_workflow_is_valid_yaml() -> None:
106
156
  "verboseLogging",
107
157
  "pollIntervalSeconds",
108
158
  "capabilities",
109
- "localConfig-apiUrl",
110
- "localConfig-portNumber",
159
+ "apiUrl",
160
+ "portNumber",
111
161
  ]
112
162
  assert document["jobs"]["build"]["uses"] == PINNED_BUILD_WORKFLOW
113
163
  assert document["jobs"]["build"]["secrets"] == "inherit"
File without changes