gd-tools-cli 0.1.0__py3-none-any.whl
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.
- gd_tools/__init__.py +3 -0
- gd_tools/__main__.py +31 -0
- gd_tools/addons/__init__.py +0 -0
- gd_tools/addons/gd-tools-coverage/coverage.gd +43 -0
- gd_tools/addons/gd-tools-coverage/post_run_hook.gd +113 -0
- gd_tools/addons/gd-tools-coverage/pre_run_hook.gd +229 -0
- gd_tools/cli.py +329 -0
- gd_tools/config.py +289 -0
- gd_tools/coverage/__init__.py +23 -0
- gd_tools/coverage/cobertura_reporter.py +136 -0
- gd_tools/coverage/html_reporter.py +95 -0
- gd_tools/coverage/lcov_reporter.py +90 -0
- gd_tools/coverage/orchestrator.py +282 -0
- gd_tools/coverage/plan_generator.py +377 -0
- gd_tools/coverage/reporter.py +518 -0
- gd_tools/coverage/templates/file.html +72 -0
- gd_tools/coverage/templates/index.html +90 -0
- gd_tools/coverage/terminal_reporter.py +107 -0
- gd_tools/doctor.py +495 -0
- gd_tools/errors.py +79 -0
- gd_tools/file_discovery.py +41 -0
- gd_tools/format_runner.py +118 -0
- gd_tools/godot.py +346 -0
- gd_tools/init.py +619 -0
- gd_tools/lint_runner.py +223 -0
- gd_tools/test_runner.py +474 -0
- gd_tools_cli-0.1.0.dist-info/METADATA +191 -0
- gd_tools_cli-0.1.0.dist-info/RECORD +31 -0
- gd_tools_cli-0.1.0.dist-info/WHEEL +5 -0
- gd_tools_cli-0.1.0.dist-info/entry_points.txt +2 -0
- gd_tools_cli-0.1.0.dist-info/top_level.txt +1 -0
gd_tools/init.py
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""Initialize a Godot project for use with gd-tools.
|
|
2
|
+
|
|
3
|
+
This module implements the ``gd-tools init`` command, which bootstraps
|
|
4
|
+
a Godot project by:
|
|
5
|
+
- Detecting the project root and Godot version
|
|
6
|
+
- Installing GUT (Godot Unit Test) if not present
|
|
7
|
+
- Deploying the coverage addon as placeholder stubs
|
|
8
|
+
- Creating configuration files (``.gutconfig.json``, ``gd-tools.toml``,
|
|
9
|
+
``gdlintrc``, ``gdformatrc``)
|
|
10
|
+
- Creating the ``.gd-tools/`` data directory
|
|
11
|
+
- Printing a summary of actions taken
|
|
12
|
+
|
|
13
|
+
The command is idempotent: running it multiple times produces the
|
|
14
|
+
same end state without duplicating entries.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import configparser
|
|
18
|
+
import json
|
|
19
|
+
import shutil
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import zipfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
import requests
|
|
27
|
+
import yaml
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
from .config import (
|
|
31
|
+
GdToolsConfig,
|
|
32
|
+
find_project_root,
|
|
33
|
+
load_config,
|
|
34
|
+
save_config,
|
|
35
|
+
)
|
|
36
|
+
from .errors import GdToolsError
|
|
37
|
+
from .godot import find_godot, get_gut_version_for_godot
|
|
38
|
+
|
|
39
|
+
# --- Constants ---
|
|
40
|
+
|
|
41
|
+
GUTCONFIG_TEMPLATE: dict = {
|
|
42
|
+
"dirs": ["res://test/", "res://tests/"],
|
|
43
|
+
"include_subdirs": True,
|
|
44
|
+
"prefix": "test_",
|
|
45
|
+
"suffix": ".gd",
|
|
46
|
+
"should_exit": True,
|
|
47
|
+
"junit_xml_file": ".gd-tools/results.xml",
|
|
48
|
+
"pre_run_script": "res://addons/gd-tools-coverage/pre_run_hook.gd",
|
|
49
|
+
"post_run_script": "res://addons/gd-tools-coverage/post_run_hook.gd",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
GUT_DOWNLOAD_URL = (
|
|
53
|
+
"https://github.com/bitwes/Gut/archive/refs/tags/v{version}.zip"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
GUT_PLUGIN_PATH = "res://addons/gut/plugin.gd"
|
|
57
|
+
|
|
58
|
+
COVERAGE_ADDON_FILES = [
|
|
59
|
+
"coverage.gd",
|
|
60
|
+
"pre_run_hook.gd",
|
|
61
|
+
"post_run_hook.gd",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
COVERAGE_AUTOLOAD_PATH = "res://addons/gd-tools-coverage/coverage.gd"
|
|
65
|
+
|
|
66
|
+
console = Console()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --- Phase 1: Project Detection and Godot Version Detection ---
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def detect_godot_version(config: GdToolsConfig) -> str:
|
|
73
|
+
"""Detect the Godot version using the config's Godot settings.
|
|
74
|
+
|
|
75
|
+
Calls :func:`find_godot` to resolve the binary and parse its
|
|
76
|
+
version. If the detected version is below 4.5.0, a warning is
|
|
77
|
+
printed but the version is still returned.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
config: The gd-tools configuration containing Godot settings.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The detected Godot version string (e.g., ``"4.5.1"``).
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
GodotNotFoundError: If the Godot binary cannot be found.
|
|
87
|
+
"""
|
|
88
|
+
info = find_godot(config.godot)
|
|
89
|
+
if not info.is_valid:
|
|
90
|
+
console.print(
|
|
91
|
+
"[yellow]Warning: Godot "
|
|
92
|
+
f"{info.version} is below the required "
|
|
93
|
+
"version 4.5.0. Some features may not "
|
|
94
|
+
"work.[/yellow]"
|
|
95
|
+
)
|
|
96
|
+
return info.version
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --- Phase 2: GUT Installation ---
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check_gut_installed(project_root: Path) -> bool:
|
|
103
|
+
"""Check if GUT is installed in the project.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
project_root: Path to the Godot project root.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if ``addons/gut/gut.gd`` exists, False otherwise.
|
|
110
|
+
"""
|
|
111
|
+
return (project_root / "addons" / "gut" / "gut.gd").exists()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_installed_gut_version(project_root: Path) -> str | None:
|
|
115
|
+
"""Get the installed GUT version from ``addons/gut/plugin.cfg``.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
project_root: Path to the Godot project root.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The GUT version string (e.g., ``"9.5.0"``), or ``None`` if
|
|
122
|
+
``plugin.cfg`` does not exist or has no ``version`` key.
|
|
123
|
+
"""
|
|
124
|
+
plugin_cfg = project_root / "addons" / "gut" / "plugin.cfg"
|
|
125
|
+
if not plugin_cfg.exists():
|
|
126
|
+
return None
|
|
127
|
+
parser = configparser.ConfigParser()
|
|
128
|
+
parser.read(plugin_cfg)
|
|
129
|
+
version = parser.get("plugin", "version", fallback=None)
|
|
130
|
+
if version is None:
|
|
131
|
+
return None
|
|
132
|
+
return version.strip('"')
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def download_gut(version: str, dest: Path) -> Path:
|
|
136
|
+
"""Download the GUT zip archive for the given version.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
version: The GUT version string (e.g., ``"9.5.0"``).
|
|
140
|
+
dest: Destination path for the downloaded zip file.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The path to the downloaded zip file.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
GdToolsError: If the download fails (network error, HTTP error,
|
|
147
|
+
etc.). The error message includes manual install instructions.
|
|
148
|
+
"""
|
|
149
|
+
url = GUT_DOWNLOAD_URL.format(version=version)
|
|
150
|
+
try:
|
|
151
|
+
response = requests.get(url, timeout=30)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
except requests.RequestException as exc:
|
|
154
|
+
raise GdToolsError(
|
|
155
|
+
f"[Error] Failed to download GUT v{version}\n"
|
|
156
|
+
f" Cause: {exc}\n"
|
|
157
|
+
f" Fix: Download GUT manually from:\n"
|
|
158
|
+
f" - Godot Asset Library: "
|
|
159
|
+
f"https://godotengine.org/asset-library/asset/116\n"
|
|
160
|
+
f" - GitHub: {url}\n"
|
|
161
|
+
f" Extract the 'addons/gut/' folder to your project's "
|
|
162
|
+
f"'addons/' directory."
|
|
163
|
+
) from exc
|
|
164
|
+
dest.write_bytes(response.content)
|
|
165
|
+
return dest
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def extract_gut(zip_path: Path, project_root: Path) -> None:
|
|
169
|
+
"""Extract the GUT zip archive and copy addons/gut/ to the project.
|
|
170
|
+
|
|
171
|
+
The GitHub archive contains a top-level directory (e.g.,
|
|
172
|
+
``Gut-9.5.0/``) with ``addons/gut/`` inside it. This function
|
|
173
|
+
extracts to a temporary directory, locates ``addons/gut/``, copies
|
|
174
|
+
it to ``project_root/addons/gut/``, and cleans up the temp dir.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
zip_path: Path to the downloaded GUT zip file.
|
|
178
|
+
project_root: Path to the Godot project root.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
GdToolsError: If the archive does not contain an
|
|
182
|
+
``addons/gut/`` directory.
|
|
183
|
+
"""
|
|
184
|
+
tmpdir = tempfile.mkdtemp()
|
|
185
|
+
try:
|
|
186
|
+
tmp_path = Path(tmpdir)
|
|
187
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
188
|
+
zf.extractall(tmp_path)
|
|
189
|
+
gut_source: Path | None = None
|
|
190
|
+
for addons_dir in tmp_path.rglob("addons"):
|
|
191
|
+
gut_dir = addons_dir / "gut"
|
|
192
|
+
if gut_dir.is_dir():
|
|
193
|
+
gut_source = gut_dir
|
|
194
|
+
break
|
|
195
|
+
if gut_source is None:
|
|
196
|
+
raise GdToolsError(
|
|
197
|
+
"[Error] Failed to extract GUT\n"
|
|
198
|
+
" Cause: addons/gut/ directory not found in archive\n"
|
|
199
|
+
" Fix: Download GUT manually from:\n"
|
|
200
|
+
" - Godot Asset Library: "
|
|
201
|
+
"https://godotengine.org/asset-library/asset/116\n"
|
|
202
|
+
" - GitHub: https://github.com/bitwes/Gut\n"
|
|
203
|
+
" Extract the 'addons/gut/' folder to your project's "
|
|
204
|
+
"'addons/' directory."
|
|
205
|
+
)
|
|
206
|
+
dest = project_root / "addons" / "gut"
|
|
207
|
+
if dest.exists():
|
|
208
|
+
shutil.rmtree(dest)
|
|
209
|
+
shutil.copytree(gut_source, dest)
|
|
210
|
+
finally:
|
|
211
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def install_gut(
|
|
215
|
+
project_root: Path, godot_version: str, non_interactive: bool
|
|
216
|
+
) -> bool:
|
|
217
|
+
"""Install GUT if not already installed.
|
|
218
|
+
|
|
219
|
+
If GUT is already installed, checks the installed version against
|
|
220
|
+
the expected version for the detected Godot version and warns if
|
|
221
|
+
they differ. If GUT is not installed, prompts the user (interactive
|
|
222
|
+
mode) or auto-installs (non-interactive mode).
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
project_root: Path to the Godot project root.
|
|
226
|
+
godot_version: The detected Godot version (e.g., ``"4.5.1"``).
|
|
227
|
+
non_interactive: If True, skip prompts and assume yes.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if GUT is installed (or was already present),
|
|
231
|
+
False if the user declined installation.
|
|
232
|
+
"""
|
|
233
|
+
if check_gut_installed(project_root):
|
|
234
|
+
installed_version = get_installed_gut_version(project_root)
|
|
235
|
+
expected_version = get_gut_version_for_godot(godot_version)
|
|
236
|
+
if installed_version != expected_version:
|
|
237
|
+
console.print(
|
|
238
|
+
"[yellow]Warning: GUT version "
|
|
239
|
+
f"{installed_version} does not match expected "
|
|
240
|
+
f"version {expected_version} for Godot "
|
|
241
|
+
f"{godot_version}.[/yellow]"
|
|
242
|
+
)
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
if not non_interactive:
|
|
246
|
+
if not click.confirm("Install GUT?", default=True):
|
|
247
|
+
console.print(
|
|
248
|
+
"GUT not installed. To install manually:\n"
|
|
249
|
+
" 1. Download from: "
|
|
250
|
+
"https://godotengine.org/asset-library/asset/116\n"
|
|
251
|
+
" 2. Extract the 'addons/gut/' folder to your "
|
|
252
|
+
"project's 'addons/' directory."
|
|
253
|
+
)
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
gut_version = get_gut_version_for_godot(godot_version)
|
|
257
|
+
tmpdir = tempfile.mkdtemp()
|
|
258
|
+
zip_dest = Path(tmpdir) / "gut.zip"
|
|
259
|
+
try:
|
|
260
|
+
download_gut(gut_version, zip_dest)
|
|
261
|
+
extract_gut(zip_dest, project_root)
|
|
262
|
+
finally:
|
|
263
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def enable_gut_plugin(project_root: Path) -> None:
|
|
268
|
+
"""Enable the GUT plugin in ``project.godot``.
|
|
269
|
+
|
|
270
|
+
Adds the ``[editor_plugins]`` section with the GUT plugin in the
|
|
271
|
+
``enabled`` list if not already present. Idempotent: running
|
|
272
|
+
multiple times produces the same result.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
project_root: Path to the Godot project root.
|
|
276
|
+
"""
|
|
277
|
+
project_godot = project_root / "project.godot"
|
|
278
|
+
content = project_godot.read_text()
|
|
279
|
+
|
|
280
|
+
gut_entry = f'"{GUT_PLUGIN_PATH}"'
|
|
281
|
+
|
|
282
|
+
# Idempotent: if GUT is already in the file, do nothing
|
|
283
|
+
if gut_entry in content:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
if "[editor_plugins]" in content:
|
|
287
|
+
# Section exists, add GUT to enabled list
|
|
288
|
+
lines = content.split("\n")
|
|
289
|
+
for i, line in enumerate(lines):
|
|
290
|
+
if line.strip() == "[editor_plugins]":
|
|
291
|
+
# Look for enabled= in subsequent lines
|
|
292
|
+
for j in range(i + 1, len(lines)):
|
|
293
|
+
next_stripped = lines[j].strip()
|
|
294
|
+
if next_stripped.startswith("["):
|
|
295
|
+
# Reached next section, insert enabled= before it
|
|
296
|
+
lines.insert(
|
|
297
|
+
j,
|
|
298
|
+
f"enabled=PackedStringArray({gut_entry})",
|
|
299
|
+
)
|
|
300
|
+
break
|
|
301
|
+
if next_stripped.startswith("enabled="):
|
|
302
|
+
# Add GUT to existing PackedStringArray
|
|
303
|
+
lines[j] = lines[j].replace(
|
|
304
|
+
"PackedStringArray(",
|
|
305
|
+
f"PackedStringArray({gut_entry}, ",
|
|
306
|
+
)
|
|
307
|
+
break
|
|
308
|
+
else:
|
|
309
|
+
# No enabled= and no next section, append at end
|
|
310
|
+
lines.append(f"enabled=PackedStringArray({gut_entry})")
|
|
311
|
+
break
|
|
312
|
+
project_godot.write_text("\n".join(lines))
|
|
313
|
+
else:
|
|
314
|
+
# No [editor_plugins] section, append it
|
|
315
|
+
if not content.endswith("\n"):
|
|
316
|
+
content += "\n"
|
|
317
|
+
content += (
|
|
318
|
+
f"\n[editor_plugins]\n\n"
|
|
319
|
+
f"enabled=PackedStringArray({gut_entry})\n"
|
|
320
|
+
)
|
|
321
|
+
project_godot.write_text(content)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# --- Phase 3: Coverage Addon Deployment ---
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def install_coverage_addon(project_root: Path) -> None:
|
|
328
|
+
"""Copy bundled coverage addon placeholder files to the project.
|
|
329
|
+
|
|
330
|
+
Copies the placeholder GDScript stubs from the package data to
|
|
331
|
+
``project_root/addons/gd-tools-coverage/``. Always overwrites
|
|
332
|
+
existing files to ensure they are up-to-date.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
project_root: Path to the Godot project root.
|
|
336
|
+
"""
|
|
337
|
+
source_dir = Path(__file__).parent / "addons" / "gd-tools-coverage"
|
|
338
|
+
target_dir = project_root / "addons" / "gd-tools-coverage"
|
|
339
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
for gd_file in COVERAGE_ADDON_FILES:
|
|
341
|
+
shutil.copy2(source_dir / gd_file, target_dir / gd_file)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def register_coverage_autoload(project_root: Path) -> None:
|
|
345
|
+
"""Register the coverage tracker autoload in ``project.godot``.
|
|
346
|
+
|
|
347
|
+
Adds ``_GDTCoverage`` to the ``[autoload]`` section, pointing at
|
|
348
|
+
``res://addons/gd-tools-coverage/coverage.gd``. Idempotent: running
|
|
349
|
+
multiple times produces the same result. If ``_GDTCoverage`` is
|
|
350
|
+
already registered with a different path, the entry is replaced
|
|
351
|
+
to avoid duplicate autoload keys.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
project_root: Path to the Godot project root.
|
|
355
|
+
"""
|
|
356
|
+
project_godot = project_root / "project.godot"
|
|
357
|
+
content = project_godot.read_text()
|
|
358
|
+
|
|
359
|
+
autoload_entry = f'_GDTCoverage="*{COVERAGE_AUTOLOAD_PATH}"'
|
|
360
|
+
|
|
361
|
+
# Idempotent: if already registered with correct path, do nothing.
|
|
362
|
+
if autoload_entry in content:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
lines = content.split("\n")
|
|
366
|
+
|
|
367
|
+
# If _GDTCoverage is already registered with a different path,
|
|
368
|
+
# replace the existing entry to avoid duplicate autoload keys.
|
|
369
|
+
for i, line in enumerate(lines):
|
|
370
|
+
if line.strip().startswith("_GDTCoverage="):
|
|
371
|
+
lines[i] = autoload_entry
|
|
372
|
+
project_godot.write_text("\n".join(lines))
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
if "[autoload]" in content:
|
|
376
|
+
# Section exists, append entry after the section header.
|
|
377
|
+
for i, line in enumerate(lines):
|
|
378
|
+
if line.strip() == "[autoload]":
|
|
379
|
+
lines.insert(i + 1, f"\n{autoload_entry}")
|
|
380
|
+
break
|
|
381
|
+
project_godot.write_text("\n".join(lines))
|
|
382
|
+
else:
|
|
383
|
+
# No [autoload] section, append it.
|
|
384
|
+
if not content.endswith("\n"):
|
|
385
|
+
content += "\n"
|
|
386
|
+
content += f"\n[autoload]\n\n{autoload_entry}\n"
|
|
387
|
+
project_godot.write_text(content)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# --- Phase 4: Configuration File Generation ---
|
|
391
|
+
|
|
392
|
+
# Keys in .gutconfig.json that are always overwritten from the template.
|
|
393
|
+
_GUTCONFIG_OVERWRITE_KEYS = (
|
|
394
|
+
"should_exit",
|
|
395
|
+
"junit_xml_file",
|
|
396
|
+
"pre_run_script",
|
|
397
|
+
"post_run_script",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Keys in .gutconfig.json that are preserved from the user's existing file.
|
|
401
|
+
_GUTCONFIG_PRESERVE_KEYS = (
|
|
402
|
+
"dirs",
|
|
403
|
+
"prefix",
|
|
404
|
+
"suffix",
|
|
405
|
+
"include_subdirs",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def update_gutconfig(project_root: Path, config: GdToolsConfig) -> None:
|
|
410
|
+
"""Create or merge ``.gutconfig.json`` in the project root.
|
|
411
|
+
|
|
412
|
+
If the file does not exist, writes ``GUTCONFIG_TEMPLATE`` as JSON.
|
|
413
|
+
If it exists, merges: preserves the user's ``dirs``, ``prefix``,
|
|
414
|
+
``suffix``, and ``include_subdirs``; always overwrites
|
|
415
|
+
``should_exit``, ``junit_xml_file``, ``pre_run_script``, and
|
|
416
|
+
``post_run_script`` from the template.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
project_root: Path to the Godot project root.
|
|
420
|
+
config: The gd-tools configuration (unused but kept for
|
|
421
|
+
signature consistency with other init functions).
|
|
422
|
+
"""
|
|
423
|
+
gutconfig_path = project_root / ".gutconfig.json"
|
|
424
|
+
|
|
425
|
+
if not gutconfig_path.exists():
|
|
426
|
+
gutconfig_path.write_text(
|
|
427
|
+
json.dumps(GUTCONFIG_TEMPLATE, indent=2) + "\n"
|
|
428
|
+
)
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
existing = json.loads(gutconfig_path.read_text())
|
|
432
|
+
merged = GUTCONFIG_TEMPLATE.copy()
|
|
433
|
+
for key in _GUTCONFIG_PRESERVE_KEYS:
|
|
434
|
+
if key in existing:
|
|
435
|
+
merged[key] = existing[key]
|
|
436
|
+
gutconfig_path.write_text(json.dumps(merged, indent=2) + "\n")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def create_config_file(project_root: Path, config: GdToolsConfig) -> None:
|
|
440
|
+
"""Create ``gd-tools.toml`` if it does not exist.
|
|
441
|
+
|
|
442
|
+
If the file already exists, it is preserved unchanged. If it
|
|
443
|
+
does not exist, the provided config is written via
|
|
444
|
+
:func:`save_config`.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
project_root: Path to the Godot project root.
|
|
448
|
+
config: The configuration to write if the file is missing.
|
|
449
|
+
"""
|
|
450
|
+
config_file = project_root / "gd-tools.toml"
|
|
451
|
+
if config_file.exists():
|
|
452
|
+
return
|
|
453
|
+
save_config(config, project_root)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def generate_lint_format_rcs(project_root: Path, config: GdToolsConfig) -> None:
|
|
457
|
+
"""Generate ``gdlintrc`` and ``gdformatrc`` if missing, warn if differs.
|
|
458
|
+
|
|
459
|
+
For each file:
|
|
460
|
+
- If the file does not exist: generate it from the config's
|
|
461
|
+
exclude lists.
|
|
462
|
+
- If the file exists but differs from what init would produce:
|
|
463
|
+
print a warning. Do not overwrite.
|
|
464
|
+
- If the file exists and matches: do nothing.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
project_root: Path to the Godot project root.
|
|
468
|
+
config: The configuration to read exclude lists from.
|
|
469
|
+
"""
|
|
470
|
+
# gdlintrc — YAML set format (same as config.generate_gdlintrc)
|
|
471
|
+
expected_lint = yaml.dump(
|
|
472
|
+
{"excluded_directories": set(config.lint.exclude)},
|
|
473
|
+
default_flow_style=False,
|
|
474
|
+
sort_keys=True,
|
|
475
|
+
)
|
|
476
|
+
lint_file = project_root / "gdlintrc"
|
|
477
|
+
if not lint_file.exists():
|
|
478
|
+
lint_file.write_text(expected_lint, encoding="utf-8")
|
|
479
|
+
elif lint_file.read_text(encoding="utf-8") != expected_lint:
|
|
480
|
+
console.print(
|
|
481
|
+
"[yellow]Warning: gdlintrc differs from expected "
|
|
482
|
+
"content. Delete it and re-run 'gd-tools init' to "
|
|
483
|
+
"regenerate.[/yellow]"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# gdformatrc — one exclude per line (same as config.generate_gdformatrc)
|
|
487
|
+
expected_format = "\n".join(config.format.exclude) + "\n"
|
|
488
|
+
format_file = project_root / "gdformatrc"
|
|
489
|
+
if not format_file.exists():
|
|
490
|
+
format_file.write_text(expected_format, encoding="utf-8")
|
|
491
|
+
elif format_file.read_text(encoding="utf-8") != expected_format:
|
|
492
|
+
console.print(
|
|
493
|
+
"[yellow]Warning: gdformatrc differs from expected "
|
|
494
|
+
"content. Delete it and re-run 'gd-tools init' to "
|
|
495
|
+
"regenerate.[/yellow]"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# --- Phase 5: Data Directory, Summary, and Orchestration ---
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def create_data_dir(project_root: Path) -> None:
|
|
503
|
+
"""Create the ``.gd-tools/`` data directory and update ``.gitignore``.
|
|
504
|
+
|
|
505
|
+
Creates ``project_root/.gd-tools/`` if it does not exist (idempotent).
|
|
506
|
+
Appends ``.gd-tools/`` to ``project_root/.gitignore``, creating the
|
|
507
|
+
file if necessary. If ``.gd-tools/`` is already present in
|
|
508
|
+
``.gitignore``, no duplicate is added.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
project_root: Path to the Godot project root.
|
|
512
|
+
"""
|
|
513
|
+
data_dir = project_root / ".gd-tools"
|
|
514
|
+
data_dir.mkdir(exist_ok=True)
|
|
515
|
+
|
|
516
|
+
gitignore = project_root / ".gitignore"
|
|
517
|
+
entry = ".gd-tools/"
|
|
518
|
+
if gitignore.exists():
|
|
519
|
+
lines = gitignore.read_text(encoding="utf-8").splitlines()
|
|
520
|
+
if entry not in lines:
|
|
521
|
+
with gitignore.open("a", encoding="utf-8") as f:
|
|
522
|
+
f.write(f"\n{entry}\n")
|
|
523
|
+
else:
|
|
524
|
+
gitignore.write_text(f"{entry}\n", encoding="utf-8")
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def print_summary(project_root: Path, actions: list[str]) -> None:
|
|
528
|
+
"""Print a Rich-formatted summary of init actions and next steps.
|
|
529
|
+
|
|
530
|
+
Lists all actions taken during initialization and prints guidance
|
|
531
|
+
on what the user should do next (e.g., run tests).
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
project_root: Path to the Godot project root.
|
|
535
|
+
actions: List of action descriptions to display.
|
|
536
|
+
"""
|
|
537
|
+
console.print("\n[bold green]gd-tools init complete![/bold green]\n")
|
|
538
|
+
console.print("[bold]Actions taken:[/bold]")
|
|
539
|
+
for action in actions:
|
|
540
|
+
console.print(f" - {action}")
|
|
541
|
+
console.print("\n[bold]Next steps:[/bold]")
|
|
542
|
+
console.print(" - Run [cyan]gd-tools test[/cyan] to execute tests")
|
|
543
|
+
console.print(" - Run [cyan]gd-tools lint[/cyan] to check code style")
|
|
544
|
+
console.print(
|
|
545
|
+
" - Run [cyan]gd-tools format[/cyan] to format GDScript files"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def run_init(non_interactive: bool = False) -> None:
|
|
550
|
+
"""Run the full init flow to bootstrap a Godot project.
|
|
551
|
+
|
|
552
|
+
Orchestrates all initialization steps:
|
|
553
|
+
1. Detect project root
|
|
554
|
+
2. Load or create config
|
|
555
|
+
3. Detect Godot version
|
|
556
|
+
4. Resolve GUT version
|
|
557
|
+
5. Check if GUT is installed
|
|
558
|
+
6. Install GUT if needed
|
|
559
|
+
7. Enable GUT plugin in project.godot
|
|
560
|
+
8. Deploy coverage addon
|
|
561
|
+
9. Create/update .gutconfig.json
|
|
562
|
+
10. Create gd-tools.toml if missing
|
|
563
|
+
11. Generate gdlintrc and gdformatrc
|
|
564
|
+
12. Create .gd-tools/ data directory
|
|
565
|
+
13. Print summary
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
non_interactive: If True, skip all interactive prompts
|
|
569
|
+
and assume defaults.
|
|
570
|
+
"""
|
|
571
|
+
project_root = find_project_root()
|
|
572
|
+
config = load_config(project_root)
|
|
573
|
+
|
|
574
|
+
actions: list[str] = []
|
|
575
|
+
|
|
576
|
+
godot_version = detect_godot_version(config)
|
|
577
|
+
gut_version = get_gut_version_for_godot(godot_version)
|
|
578
|
+
|
|
579
|
+
is_installed = check_gut_installed(project_root)
|
|
580
|
+
if is_installed:
|
|
581
|
+
installed = get_installed_gut_version(project_root)
|
|
582
|
+
if installed:
|
|
583
|
+
actions.append(f"GUT already installed (v{installed})")
|
|
584
|
+
else:
|
|
585
|
+
actions.append("GUT already installed (version unknown)")
|
|
586
|
+
else:
|
|
587
|
+
actions.append(f"Installing GUT v{gut_version}")
|
|
588
|
+
|
|
589
|
+
if not install_gut(
|
|
590
|
+
project_root, godot_version, non_interactive=non_interactive
|
|
591
|
+
):
|
|
592
|
+
console.print(
|
|
593
|
+
"\n[yellow]Init aborted: GUT was not installed.[/yellow]\n"
|
|
594
|
+
"Install GUT manually, then re-run 'gd-tools init'."
|
|
595
|
+
)
|
|
596
|
+
sys.exit(0)
|
|
597
|
+
|
|
598
|
+
enable_gut_plugin(project_root)
|
|
599
|
+
actions.append("Enabled GUT plugin in project.godot")
|
|
600
|
+
|
|
601
|
+
install_coverage_addon(project_root)
|
|
602
|
+
actions.append("Deployed coverage addon")
|
|
603
|
+
|
|
604
|
+
register_coverage_autoload(project_root)
|
|
605
|
+
actions.append("Registered _GDTCoverage autoload")
|
|
606
|
+
|
|
607
|
+
update_gutconfig(project_root, config)
|
|
608
|
+
actions.append("Created/updated .gutconfig.json")
|
|
609
|
+
|
|
610
|
+
create_config_file(project_root, config)
|
|
611
|
+
actions.append("Ensured gd-tools.toml exists")
|
|
612
|
+
|
|
613
|
+
generate_lint_format_rcs(project_root, config)
|
|
614
|
+
actions.append("Generated gdlintrc and gdformatrc")
|
|
615
|
+
|
|
616
|
+
create_data_dir(project_root)
|
|
617
|
+
actions.append("Created .gd-tools/ directory")
|
|
618
|
+
|
|
619
|
+
print_summary(project_root, actions)
|