gdharness 1.2.34 → 1.2.35
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.
- package/build/cli.js +85 -14
- package/build/godot/operations/export_presets.gd +232 -0
- package/build/godot/operations/godot_operations.gd +4 -1
- package/build/godot/operations/import_pipeline.gd +4 -60
- package/build/godot/operations/project_diagnostics.gd +3 -1
- package/build/index.js +84 -13
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8141,9 +8141,14 @@ class GameLog {
|
|
|
8141
8141
|
const owner = this.lastHeadline[source];
|
|
8142
8142
|
if (owner !== null && /^\s/.test(line)) {
|
|
8143
8143
|
owner.detail.push(line.trim());
|
|
8144
|
+
owner.continues = false;
|
|
8144
8145
|
return;
|
|
8145
8146
|
}
|
|
8146
8147
|
const headline = announced(line);
|
|
8148
|
+
if (owner?.continues === true && headline === null) {
|
|
8149
|
+
owner.detail.push(line.trim());
|
|
8150
|
+
return;
|
|
8151
|
+
}
|
|
8147
8152
|
const detail = [];
|
|
8148
8153
|
const entry = {
|
|
8149
8154
|
index: this.entries.length,
|
|
@@ -8156,7 +8161,7 @@ class GameLog {
|
|
|
8156
8161
|
if (headline?.byScript === true) {
|
|
8157
8162
|
this.byScript.add(entry.index);
|
|
8158
8163
|
}
|
|
8159
|
-
this.lastHeadline[source] = headline ? { detail } : null;
|
|
8164
|
+
this.lastHeadline[source] = headline ? { detail, continues: source === "stderr" && headline.text.endsWith(":") } : null;
|
|
8160
8165
|
}
|
|
8161
8166
|
raisedByScript(entry) {
|
|
8162
8167
|
return this.byScript.has(entry.index);
|
|
@@ -11558,20 +11563,36 @@ var init_tool_definitions = __esm(() => {
|
|
|
11558
11563
|
{
|
|
11559
11564
|
name: "project_export",
|
|
11560
11565
|
title: "Export",
|
|
11561
|
-
description: "Export presets and exports.",
|
|
11566
|
+
description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
|
|
11562
11567
|
parameters: {
|
|
11563
11568
|
projectPath: PROJECT_PATH,
|
|
11564
|
-
preset: {
|
|
11569
|
+
preset: {
|
|
11570
|
+
type: "string",
|
|
11571
|
+
ops: ["run", "add_preset"],
|
|
11572
|
+
description: "Preset name from export_presets.cfg. add_preset: the name of the new one."
|
|
11573
|
+
},
|
|
11574
|
+
platform: {
|
|
11575
|
+
type: "string",
|
|
11576
|
+
ops: ["add_preset"],
|
|
11577
|
+
enum: ["Windows Desktop", "Linux", "macOS", "Android", "iOS", "Web"],
|
|
11578
|
+
description: "add_preset: the platform the preset exports for."
|
|
11579
|
+
},
|
|
11565
11580
|
outputPath: {
|
|
11566
11581
|
type: "string",
|
|
11567
|
-
|
|
11582
|
+
ops: ["run", "add_preset"],
|
|
11583
|
+
description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset. add_preset: the path the preset exports to from the editor, optional.`
|
|
11568
11584
|
},
|
|
11569
|
-
debug: { type: "boolean", description: "run: a debug export. Default false." }
|
|
11585
|
+
debug: { type: "boolean", ops: ["run"], description: "run: a debug export. Default false." }
|
|
11570
11586
|
},
|
|
11571
11587
|
requires: ["projectPath"],
|
|
11572
11588
|
operations: {
|
|
11573
11589
|
list: { effect: "read", summary: "the presets in export_presets.cfg", requires: [] },
|
|
11574
|
-
run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] }
|
|
11590
|
+
run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] },
|
|
11591
|
+
add_preset: {
|
|
11592
|
+
effect: "additive",
|
|
11593
|
+
summary: "add an export preset to export_presets.cfg",
|
|
11594
|
+
requires: ["preset", "platform"]
|
|
11595
|
+
}
|
|
11575
11596
|
}
|
|
11576
11597
|
},
|
|
11577
11598
|
{
|
|
@@ -12946,7 +12967,7 @@ var init_headless_operations = __esm(() => {
|
|
|
12946
12967
|
uid: "get_uid",
|
|
12947
12968
|
refresh_classes: "refresh_class_cache"
|
|
12948
12969
|
}),
|
|
12949
|
-
project_export: dictionary({ list: "list_export_presets" }),
|
|
12970
|
+
project_export: dictionary({ list: "list_export_presets", add_preset: "add_export_preset" }),
|
|
12950
12971
|
script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
|
|
12951
12972
|
script_info: dictionary({ structure: "get_script_info" }),
|
|
12952
12973
|
editor_classes: dictionary({
|
|
@@ -44892,7 +44913,7 @@ function exportPresetRefusal(projectPath, preset) {
|
|
|
44892
44913
|
try {
|
|
44893
44914
|
text = readFileSync26(join30(projectPath, "export_presets.cfg"), "utf8");
|
|
44894
44915
|
} catch {
|
|
44895
|
-
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with.
|
|
44916
|
+
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
|
|
44896
44917
|
}
|
|
44897
44918
|
const names = [];
|
|
44898
44919
|
let inPreset = false;
|
|
@@ -44910,9 +44931,9 @@ function exportPresetRefusal(projectPath, preset) {
|
|
|
44910
44931
|
if (names.includes(preset)) {
|
|
44911
44932
|
return null;
|
|
44912
44933
|
}
|
|
44913
|
-
return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with.
|
|
44934
|
+
return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. project_export add_preset adds one.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
|
|
44914
44935
|
}
|
|
44915
|
-
function exportAnswer(ending, asked, before, after) {
|
|
44936
|
+
function exportAnswer(ending, asked, before, after, beside = []) {
|
|
44916
44937
|
const { log } = ending;
|
|
44917
44938
|
const written = after !== null && after !== before;
|
|
44918
44939
|
const notCopied = librariesNotCopied(log.all);
|
|
@@ -44924,6 +44945,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
44924
44945
|
failure: ending.failure ?? undefined,
|
|
44925
44946
|
errors: log.count("error"),
|
|
44926
44947
|
warnings: log.count("warning"),
|
|
44948
|
+
...beside.length === 0 ? {} : { alsoWritten: beside },
|
|
44927
44949
|
...notCopied.length === 0 ? {} : {
|
|
44928
44950
|
librariesNotLoaded: notCopied,
|
|
44929
44951
|
extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
|
|
@@ -44946,6 +44968,20 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
44946
44968
|
isError: true
|
|
44947
44969
|
};
|
|
44948
44970
|
}
|
|
44971
|
+
function writtenBeside(before, after, output, within) {
|
|
44972
|
+
return [...after].filter(([name, at]) => name !== output && before.get(name) !== at).map(([name]) => within === "." ? name : `${within}/${name}`).sort();
|
|
44973
|
+
}
|
|
44974
|
+
function filesIn(folder) {
|
|
44975
|
+
const found = new Map;
|
|
44976
|
+
try {
|
|
44977
|
+
for (const entry of readdirSync13(folder, { withFileTypes: true })) {
|
|
44978
|
+
if (entry.isFile()) {
|
|
44979
|
+
found.set(entry.name, statSync13(join30(folder, entry.name)).mtimeMs);
|
|
44980
|
+
}
|
|
44981
|
+
}
|
|
44982
|
+
} catch {}
|
|
44983
|
+
return found;
|
|
44984
|
+
}
|
|
44949
44985
|
function scanWaitAnswer(scanned, started = "game") {
|
|
44950
44986
|
if (scanned.waitedMs === 0) {
|
|
44951
44987
|
return {};
|
|
@@ -46180,6 +46216,9 @@ class GodotServer {
|
|
|
46180
46216
|
if (tool === "project_import" && op === "set_options") {
|
|
46181
46217
|
return await this.handleSetImportOptions(args);
|
|
46182
46218
|
}
|
|
46219
|
+
if (tool === "project_export" && op === "add_preset") {
|
|
46220
|
+
return await this.handleAddExportPreset(args);
|
|
46221
|
+
}
|
|
46183
46222
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
46184
46223
|
if (headless !== undefined) {
|
|
46185
46224
|
const { from: asked, ...answerable } = args;
|
|
@@ -47243,6 +47282,30 @@ class GodotServer {
|
|
|
47243
47282
|
return this.createErrorResponse(`Search failed: ${errorMessage(error)}`);
|
|
47244
47283
|
}
|
|
47245
47284
|
}
|
|
47285
|
+
async handleAddExportPreset(args) {
|
|
47286
|
+
const project = this.project(args);
|
|
47287
|
+
if (!project.ok) {
|
|
47288
|
+
return project.response;
|
|
47289
|
+
}
|
|
47290
|
+
const given = readNonEmptyString(args, "outputPath");
|
|
47291
|
+
const output = given === undefined ? null : resolveWithinProject(project.value.path, given);
|
|
47292
|
+
if (output !== null && !output.ok) {
|
|
47293
|
+
return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
|
|
47294
|
+
}
|
|
47295
|
+
const outcome = await this.operation("add_export_preset", {
|
|
47296
|
+
preset: readString(args, "preset") ?? "",
|
|
47297
|
+
platform: readString(args, "platform") ?? "",
|
|
47298
|
+
outputPath: output?.relativePath ?? ""
|
|
47299
|
+
}, project.value.path);
|
|
47300
|
+
if (!outcome.ok || !this.editorHasOpen(project.value.path)) {
|
|
47301
|
+
return this.answer(outcome);
|
|
47302
|
+
}
|
|
47303
|
+
return this.jsonTextResponse({
|
|
47304
|
+
...outcome.payload,
|
|
47305
|
+
editorNote: "The editor open on this project read export_presets.cfg when it started and does not list this preset, and saving presets in its Export dialog writes its own list over the file. editor_launch restart loads the file; an export through project_export run reads the file and needs no restart.",
|
|
47306
|
+
...engineExtras(outcome)
|
|
47307
|
+
});
|
|
47308
|
+
}
|
|
47246
47309
|
async handleExportProject(args) {
|
|
47247
47310
|
const project = this.project(args);
|
|
47248
47311
|
if (!project.ok) {
|
|
@@ -47251,11 +47314,14 @@ class GodotServer {
|
|
|
47251
47314
|
const preset = readNonEmptyString(args, "preset") ?? "";
|
|
47252
47315
|
const output = resolveWithinProject(project.value.path, readString(args, "outputPath") ?? "");
|
|
47253
47316
|
if (!output.ok) {
|
|
47254
|
-
return this.createErrorResponse(output.reason,
|
|
47317
|
+
return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
|
|
47255
47318
|
}
|
|
47256
47319
|
const noPreset = exportPresetRefusal(project.value.path, preset);
|
|
47257
47320
|
if (noPreset !== null) {
|
|
47258
|
-
return this.createErrorResponse(noPreset, [
|
|
47321
|
+
return this.createErrorResponse(noPreset, [
|
|
47322
|
+
"project_export list reads the presets the project has",
|
|
47323
|
+
"project_export add_preset adds one"
|
|
47324
|
+
]);
|
|
47259
47325
|
}
|
|
47260
47326
|
const engine = await this.engine();
|
|
47261
47327
|
if (!engine.ok) {
|
|
@@ -47282,6 +47348,7 @@ class GodotServer {
|
|
|
47282
47348
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
47283
47349
|
const writtenAt = () => existsSync23(output.absolutePath) ? statSync13(output.absolutePath).mtimeMs : null;
|
|
47284
47350
|
const before = writtenAt();
|
|
47351
|
+
const listedBefore = filesIn(folder);
|
|
47285
47352
|
const freed = freeLibraryCopies(project.value.path);
|
|
47286
47353
|
if (freed.setAside.length > 0 || freed.swept.length > 0) {
|
|
47287
47354
|
this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
|
|
@@ -47295,7 +47362,8 @@ class GodotServer {
|
|
|
47295
47362
|
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
47296
47363
|
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
47297
47364
|
}
|
|
47298
|
-
|
|
47365
|
+
const beside = writtenBeside(listedBefore, filesIn(folder), basename9(output.absolutePath), dirname22(output.relativePath).replaceAll("\\", "/"));
|
|
47366
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside);
|
|
47299
47367
|
}
|
|
47300
47368
|
reportTestProgress(log) {
|
|
47301
47369
|
const progress = callProgress();
|
|
@@ -51385,7 +51453,7 @@ async function runGodotServer() {
|
|
|
51385
51453
|
const server = new GodotServer;
|
|
51386
51454
|
await server.run();
|
|
51387
51455
|
}
|
|
51388
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
51456
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
51389
51457
|
var init_server = __esm(() => {
|
|
51390
51458
|
init_dist();
|
|
51391
51459
|
init_stdio();
|
|
@@ -51467,6 +51535,9 @@ var init_server = __esm(() => {
|
|
|
51467
51535
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
51468
51536
|
"Point projectPath at the project the file belongs to"
|
|
51469
51537
|
];
|
|
51538
|
+
OUTPUT_PATH_SOLUTIONS = [
|
|
51539
|
+
'Give outputPath inside the project, relative to it, such as "build/game.exe" or "res://build/game.exe"; copy the build elsewhere once it is exported'
|
|
51540
|
+
];
|
|
51470
51541
|
PROJECT_FILE_ARGUMENTS = [
|
|
51471
51542
|
"scenePath",
|
|
51472
51543
|
"instancePath",
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## export_presets.cfg, read and written in the format the editor's Project > Export saves it in.
|
|
4
|
+
|
|
5
|
+
const Read = preload("reading.gd")
|
|
6
|
+
const Log = preload("logger.gd")
|
|
7
|
+
|
|
8
|
+
const PRESETS_FILE: String = "res://export_presets.cfg"
|
|
9
|
+
## Where the editor names the one preset of each platform it runs on a device.
|
|
10
|
+
const RUNNABLE_SECTION: String = "runnable_presets"
|
|
11
|
+
## The platforms the editor's exporter offers, by the name a preset gives its platform, each with
|
|
12
|
+
## how the names of its files in the export templates directory begin.
|
|
13
|
+
const PLATFORMS: Dictionary[String, String] = {
|
|
14
|
+
"Windows Desktop": "windows_",
|
|
15
|
+
"Linux": "linux_",
|
|
16
|
+
"macOS": "macos",
|
|
17
|
+
"Android": "android_",
|
|
18
|
+
"iOS": "ios",
|
|
19
|
+
"Web": "web_",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var _log: Log
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
func _init(p_log: Log) -> void:
|
|
26
|
+
_log = p_log
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## The names of the project's export presets, in the order export_presets.cfg holds them.
|
|
30
|
+
static func names() -> Array[String]:
|
|
31
|
+
var named: Array[String] = []
|
|
32
|
+
var config: ConfigFile = ConfigFile.new()
|
|
33
|
+
if config.load(PRESETS_FILE) != OK:
|
|
34
|
+
return named
|
|
35
|
+
var index: int = 0
|
|
36
|
+
while config.has_section("preset." + str(index)):
|
|
37
|
+
named.append(str(config.get_value("preset." + str(index), "name", "")))
|
|
38
|
+
index += 1
|
|
39
|
+
return named
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
func list_export_presets(_params: Dictionary) -> Dictionary:
|
|
43
|
+
if not FileAccess.file_exists(PRESETS_FILE):
|
|
44
|
+
return {
|
|
45
|
+
"presets": [],
|
|
46
|
+
"presets_file_exists": false,
|
|
47
|
+
"note":
|
|
48
|
+
(
|
|
49
|
+
"No export_presets.cfg found. project_export add_preset adds a preset, as the editor's "
|
|
50
|
+
+ "Project > Export does."
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
var config: ConfigFile = ConfigFile.new()
|
|
54
|
+
var err: Error = config.load(PRESETS_FILE)
|
|
55
|
+
if err != OK:
|
|
56
|
+
return _log.failure("Failed to parse export_presets.cfg: " + error_string(err))
|
|
57
|
+
|
|
58
|
+
var templates: String = _templates_dir()
|
|
59
|
+
var presets: Array[Dictionary] = []
|
|
60
|
+
var index: int = 0
|
|
61
|
+
while config.has_section("preset." + str(index)):
|
|
62
|
+
var section: String = "preset." + str(index)
|
|
63
|
+
var platform: String = str(config.get_value(section, "platform", "Unknown"))
|
|
64
|
+
var preset: Dictionary = {
|
|
65
|
+
"index": index,
|
|
66
|
+
"name": config.get_value(section, "name", "Unknown"),
|
|
67
|
+
"platform": platform,
|
|
68
|
+
"runnable": _runnable(config, section),
|
|
69
|
+
"export_path": config.get_value(section, "export_path", ""),
|
|
70
|
+
"export_filter": config.get_value(section, "export_filter", "all_resources"),
|
|
71
|
+
"include_filter": config.get_value(section, "include_filter", ""),
|
|
72
|
+
"exclude_filter": config.get_value(section, "exclude_filter", "")
|
|
73
|
+
}
|
|
74
|
+
if config.has_section_key(section, "custom_features"):
|
|
75
|
+
preset["custom_features"] = config.get_value(section, "custom_features", "")
|
|
76
|
+
# Left out for a platform this file names and the exporter does not know, which has no
|
|
77
|
+
# templates to look for.
|
|
78
|
+
if platform in PLATFORMS:
|
|
79
|
+
preset["templates_installed"] = _installed(templates, platform)
|
|
80
|
+
presets.append(preset)
|
|
81
|
+
index += 1
|
|
82
|
+
return {"presets": presets, "presets_file_exists": true, "total_presets": index, "templates": templates}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Whether the preset in [param section] is the one its platform runs. The editor names that preset
|
|
86
|
+
## in a section of its own, and kept a key in the preset's section only in files saved before that.
|
|
87
|
+
static func _runnable(config: ConfigFile, section: String) -> bool:
|
|
88
|
+
var platform: String = str(config.get_value(section, "platform", ""))
|
|
89
|
+
if config.has_section_key(RUNNABLE_SECTION, platform):
|
|
90
|
+
return str(config.get_value(RUNNABLE_SECTION, platform)) == str(config.get_value(section, "name", ""))
|
|
91
|
+
return Read.as_bool(config.get_value(section, "runnable", false))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Adds an export preset to export_presets.cfg, written with the keys the editor's Project > Export
|
|
95
|
+
## writes for a new one. The editor's exporter fills every platform option the file leaves out with
|
|
96
|
+
## its default when it loads the file, which is what a preset fresh from that dialog holds too.
|
|
97
|
+
func add_export_preset(params: Dictionary) -> Dictionary:
|
|
98
|
+
var preset_name: String = str(params.get("preset", "")).strip_edges()
|
|
99
|
+
var platform: String = str(params.get("platform", ""))
|
|
100
|
+
var export_path: String = str(params.get("output_path", "")).trim_prefix("res://")
|
|
101
|
+
if preset_name.is_empty():
|
|
102
|
+
return _log.failure("preset is required: the name the preset is exported by")
|
|
103
|
+
if platform not in PLATFORMS:
|
|
104
|
+
return _log.failure(
|
|
105
|
+
"Unknown platform '%s'. The platforms are %s." % [platform, ", ".join(PLATFORMS.keys())]
|
|
106
|
+
)
|
|
107
|
+
var config: ConfigFile = ConfigFile.new()
|
|
108
|
+
if FileAccess.file_exists(PRESETS_FILE):
|
|
109
|
+
var loading: Error = config.load(PRESETS_FILE)
|
|
110
|
+
if loading != OK:
|
|
111
|
+
return _log.failure("Failed to parse export_presets.cfg: " + error_string(loading))
|
|
112
|
+
var index: int = 0
|
|
113
|
+
while config.has_section("preset." + str(index)):
|
|
114
|
+
var held: String = "preset." + str(index)
|
|
115
|
+
if str(config.get_value(held, "name", "")) == preset_name:
|
|
116
|
+
return _log.failure(
|
|
117
|
+
(
|
|
118
|
+
(
|
|
119
|
+
"There is already an export preset named '%s', for %s, exporting to '%s'. "
|
|
120
|
+
+ "project_export run exports with it; give another name to add a second."
|
|
121
|
+
)
|
|
122
|
+
% [
|
|
123
|
+
preset_name,
|
|
124
|
+
config.get_value(held, "platform", ""),
|
|
125
|
+
config.get_value(held, "export_path", "")
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
index += 1
|
|
130
|
+
# A platform with no preset to run takes the new one, as the editor's dialog does.
|
|
131
|
+
var runnable: bool = not config.has_section_key(RUNNABLE_SECTION, platform)
|
|
132
|
+
if runnable:
|
|
133
|
+
config.set_value(RUNNABLE_SECTION, platform, preset_name)
|
|
134
|
+
|
|
135
|
+
var section: String = "preset." + str(index)
|
|
136
|
+
var values: Dictionary = {
|
|
137
|
+
"name": preset_name,
|
|
138
|
+
"platform": platform,
|
|
139
|
+
"dedicated_server": false,
|
|
140
|
+
"custom_features": "",
|
|
141
|
+
"export_filter": "all_resources",
|
|
142
|
+
"include_filter": "",
|
|
143
|
+
"exclude_filter": "",
|
|
144
|
+
"export_path": export_path,
|
|
145
|
+
"patches": PackedStringArray(),
|
|
146
|
+
"encryption_include_filters": "",
|
|
147
|
+
"encryption_exclude_filters": "",
|
|
148
|
+
"seed": 0,
|
|
149
|
+
"encrypt_pck": false,
|
|
150
|
+
"encrypt_directory": false,
|
|
151
|
+
"script_export_mode": 2,
|
|
152
|
+
}
|
|
153
|
+
for key: String in values:
|
|
154
|
+
config.set_value(section, key, values[key])
|
|
155
|
+
# A section with nothing in it is not written, and the editor's loader reports a missing options
|
|
156
|
+
# section as an error; these two are options of every platform, empty in a new preset.
|
|
157
|
+
config.set_value(section + ".options", "custom_template/debug", "")
|
|
158
|
+
config.set_value(section + ".options", "custom_template/release", "")
|
|
159
|
+
var saving: Error = config.save(PRESETS_FILE)
|
|
160
|
+
if saving != OK:
|
|
161
|
+
return _log.failure("Failed to save export_presets.cfg: " + error_string(saving))
|
|
162
|
+
|
|
163
|
+
var templates: String = _templates_dir()
|
|
164
|
+
var installed: bool = _installed(templates, platform)
|
|
165
|
+
var answer: Dictionary = {
|
|
166
|
+
"preset": preset_name,
|
|
167
|
+
"platform": platform,
|
|
168
|
+
"index": index,
|
|
169
|
+
"runnable": runnable,
|
|
170
|
+
"export_path": export_path,
|
|
171
|
+
"file": PRESETS_FILE,
|
|
172
|
+
"templates": templates,
|
|
173
|
+
"templates_installed": installed,
|
|
174
|
+
}
|
|
175
|
+
if not installed:
|
|
176
|
+
answer["templates_note"] = (
|
|
177
|
+
(
|
|
178
|
+
"No %s export templates for this engine are at %s, and an export for %s needs them. The "
|
|
179
|
+
+ "editor installs them in Editor > Manage Export Templates, or they can be unpacked "
|
|
180
|
+
+ "there from the export templates archive of this engine's release."
|
|
181
|
+
)
|
|
182
|
+
% [platform, templates, platform]
|
|
183
|
+
)
|
|
184
|
+
return answer
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
## Where the editor of this engine keeps its export templates: beside the executable for a
|
|
188
|
+
## self-contained one, otherwise in the user's data directory, as the editor's own paths have it.
|
|
189
|
+
static func _templates_dir() -> String:
|
|
190
|
+
var version: Dictionary = Engine.get_version_info()
|
|
191
|
+
var named: String = "%d.%d" % [Read.as_int(version.get("major", 0)), Read.as_int(version.get("minor", 0))]
|
|
192
|
+
if Read.as_int(version.get("patch", 0)) != 0:
|
|
193
|
+
named += "." + str(Read.as_int(version.get("patch", 0)))
|
|
194
|
+
named += "." + str(version.get("status", ""))
|
|
195
|
+
if ClassDB.class_exists("CSharpScript"):
|
|
196
|
+
named += ".mono"
|
|
197
|
+
var beside: String = OS.get_executable_path().get_base_dir()
|
|
198
|
+
var contained: bool = _marks_self_contained(beside)
|
|
199
|
+
# A macOS bundle is read-only, so its editor looks for the marker beside the bundle too, and keeps
|
|
200
|
+
# its data there whichever of the two places had it.
|
|
201
|
+
if (
|
|
202
|
+
OS.has_feature("macos")
|
|
203
|
+
and beside.ends_with("MacOS")
|
|
204
|
+
and beside.path_join("..").simplify_path().ends_with("Contents")
|
|
205
|
+
):
|
|
206
|
+
beside = beside.path_join("../../..").simplify_path()
|
|
207
|
+
contained = contained or _marks_self_contained(beside)
|
|
208
|
+
if contained:
|
|
209
|
+
return beside.path_join("editor_data").path_join("export_templates").path_join(named)
|
|
210
|
+
var home: String = (
|
|
211
|
+
"godot" if OS.get_name() in ["Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD"] else "Godot"
|
|
212
|
+
)
|
|
213
|
+
return OS.get_data_dir().path_join(home).path_join("export_templates").path_join(named)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
## Whether [param templates] holds any of [param platform]'s templates. The directory is one per
|
|
217
|
+
## engine version and holds whichever platforms were installed into it, so its being there says
|
|
218
|
+
## nothing about this platform.
|
|
219
|
+
static func _installed(templates: String, platform: String) -> bool:
|
|
220
|
+
if not DirAccess.dir_exists_absolute(templates):
|
|
221
|
+
return false
|
|
222
|
+
for file: String in DirAccess.get_files_at(templates):
|
|
223
|
+
if file.begins_with(PLATFORMS[platform]):
|
|
224
|
+
return true
|
|
225
|
+
return false
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
static func _marks_self_contained(directory: String) -> bool:
|
|
229
|
+
return (
|
|
230
|
+
FileAccess.file_exists(directory.path_join("._sc_"))
|
|
231
|
+
or FileAccess.file_exists(directory.path_join("_sc_"))
|
|
232
|
+
)
|
|
@@ -19,6 +19,7 @@ const AudioBuses = preload("audio_buses.gd")
|
|
|
19
19
|
const ClassCache = preload("class_cache.gd")
|
|
20
20
|
const ClassDbQueries = preload("classdb_queries.gd")
|
|
21
21
|
const Dependencies = preload("dependencies.gd")
|
|
22
|
+
const ExportPresets = preload("export_presets.gd")
|
|
22
23
|
const FileReads = preload("file_reads.gd")
|
|
23
24
|
const GdscriptAnalysis = preload("gdscript_analysis.gd")
|
|
24
25
|
const GdscriptAuthoring = preload("gdscript_authoring.gd")
|
|
@@ -179,7 +180,9 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
179
180
|
"set_import_options":
|
|
180
181
|
payload = ImportPipeline.new(_log).set_import_options(params)
|
|
181
182
|
"list_export_presets":
|
|
182
|
-
payload =
|
|
183
|
+
payload = ExportPresets.new(_log).list_export_presets(params)
|
|
184
|
+
"add_export_preset":
|
|
185
|
+
payload = ExportPresets.new(_log).add_export_preset(params)
|
|
183
186
|
"validate_project":
|
|
184
187
|
payload = ImportPipeline.new(_log).validate_project(params)
|
|
185
188
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const ExportPresets = preload("export_presets.gd")
|
|
3
4
|
const Read = preload("reading.gd")
|
|
4
5
|
const Serialisation = preload("serialisation.gd")
|
|
5
6
|
const FileWalk = preload("file_walk.gd")
|
|
@@ -319,63 +320,6 @@ static func _all_ints(held: Variant, keys: bool) -> bool:
|
|
|
319
320
|
return true
|
|
320
321
|
|
|
321
322
|
|
|
322
|
-
# The names of the project's export presets, in the order export_presets.cfg holds them.
|
|
323
|
-
static func _preset_names() -> Array[String]:
|
|
324
|
-
var names: Array[String] = []
|
|
325
|
-
var config: ConfigFile = ConfigFile.new()
|
|
326
|
-
if config.load("res://export_presets.cfg") != OK:
|
|
327
|
-
return names
|
|
328
|
-
var index: int = 0
|
|
329
|
-
while config.has_section("preset." + str(index)):
|
|
330
|
-
names.append(str(config.get_value("preset." + str(index), "name", "")))
|
|
331
|
-
index += 1
|
|
332
|
-
return names
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
# List export presets
|
|
336
|
-
func list_export_presets(_params: Dictionary) -> Dictionary:
|
|
337
|
-
_log.info("Listing export presets")
|
|
338
|
-
|
|
339
|
-
var presets_file: String = "res://export_presets.cfg"
|
|
340
|
-
|
|
341
|
-
if not FileAccess.file_exists(presets_file):
|
|
342
|
-
return {
|
|
343
|
-
"presets": [],
|
|
344
|
-
"presets_file_exists": false,
|
|
345
|
-
"note": "No export_presets.cfg found. Configure export presets in the Godot editor."
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
var config: ConfigFile = ConfigFile.new()
|
|
349
|
-
var err: Error = config.load(presets_file)
|
|
350
|
-
|
|
351
|
-
if err != OK:
|
|
352
|
-
return _log.failure("Failed to parse export_presets.cfg: " + str(err))
|
|
353
|
-
|
|
354
|
-
# Export presets are stored as [preset.0], [preset.1], etc.
|
|
355
|
-
var presets: Array[Dictionary] = []
|
|
356
|
-
var preset_idx: int = 0
|
|
357
|
-
while config.has_section("preset." + str(preset_idx)):
|
|
358
|
-
var section: String = "preset." + str(preset_idx)
|
|
359
|
-
var preset: Dictionary = {
|
|
360
|
-
"index": preset_idx,
|
|
361
|
-
"name": config.get_value(section, "name", "Unknown"),
|
|
362
|
-
"platform": config.get_value(section, "platform", "Unknown"),
|
|
363
|
-
"runnable": config.get_value(section, "runnable", false),
|
|
364
|
-
"export_path": config.get_value(section, "export_path", ""),
|
|
365
|
-
"export_filter": config.get_value(section, "export_filter", "all_resources"),
|
|
366
|
-
"include_filter": config.get_value(section, "include_filter", ""),
|
|
367
|
-
"exclude_filter": config.get_value(section, "exclude_filter", "")
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if config.has_section_key(section, "custom_features"):
|
|
371
|
-
preset["custom_features"] = config.get_value(section, "custom_features", "")
|
|
372
|
-
|
|
373
|
-
presets.append(preset)
|
|
374
|
-
preset_idx += 1
|
|
375
|
-
|
|
376
|
-
return {"presets": presets, "presets_file_exists": true, "total_presets": preset_idx}
|
|
377
|
-
|
|
378
|
-
|
|
379
323
|
# Validate project for export
|
|
380
324
|
func validate_project(params: Dictionary) -> Dictionary:
|
|
381
325
|
var preset_name: String = str(params.get("preset", ""))
|
|
@@ -429,7 +373,7 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
429
373
|
"warning",
|
|
430
374
|
"export_presets",
|
|
431
375
|
"No export presets configured",
|
|
432
|
-
"
|
|
376
|
+
"Add a preset with project_export add_preset, or in the editor: Project > Export",
|
|
433
377
|
include_suggestions
|
|
434
378
|
)
|
|
435
379
|
)
|
|
@@ -438,7 +382,7 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
438
382
|
# does not exist answered valid, as though it had been checked.
|
|
439
383
|
if not preset_name.is_empty():
|
|
440
384
|
checks_performed.append("export_preset")
|
|
441
|
-
var named: Array[String] =
|
|
385
|
+
var named: Array[String] = ExportPresets.names()
|
|
442
386
|
if preset_name not in named:
|
|
443
387
|
issues.append(
|
|
444
388
|
_finding(
|
|
@@ -453,7 +397,7 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
453
397
|
else ": the presets are " + ", ".join(named)
|
|
454
398
|
)
|
|
455
399
|
),
|
|
456
|
-
"Pass one of the preset names, or add the preset
|
|
400
|
+
"Pass one of the preset names, or add the preset with project_export add_preset",
|
|
457
401
|
include_suggestions
|
|
458
402
|
)
|
|
459
403
|
)
|
|
@@ -148,7 +148,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
148
148
|
recommendations.append("Address critical issues to improve project stability")
|
|
149
149
|
# A project still being made has no presets yet, which is no fault in it.
|
|
150
150
|
if "config" in check_categories and not FileAccess.file_exists("res://export_presets.cfg"):
|
|
151
|
-
recommendations.append(
|
|
151
|
+
recommendations.append(
|
|
152
|
+
"No export presets are configured: project_export add_preset adds one before exporting"
|
|
153
|
+
)
|
|
152
154
|
|
|
153
155
|
# Every detail costs points, so each is an issue; `issues` was always empty beside them, and a
|
|
154
156
|
# category said passed next to the failure it had just found.
|
package/build/index.js
CHANGED
|
@@ -28717,9 +28717,14 @@ class GameLog {
|
|
|
28717
28717
|
const owner = this.lastHeadline[source];
|
|
28718
28718
|
if (owner !== null && /^\s/.test(line)) {
|
|
28719
28719
|
owner.detail.push(line.trim());
|
|
28720
|
+
owner.continues = false;
|
|
28720
28721
|
return;
|
|
28721
28722
|
}
|
|
28722
28723
|
const headline = announced(line);
|
|
28724
|
+
if (owner?.continues === true && headline === null) {
|
|
28725
|
+
owner.detail.push(line.trim());
|
|
28726
|
+
return;
|
|
28727
|
+
}
|
|
28723
28728
|
const detail = [];
|
|
28724
28729
|
const entry = {
|
|
28725
28730
|
index: this.entries.length,
|
|
@@ -28732,7 +28737,7 @@ class GameLog {
|
|
|
28732
28737
|
if (headline?.byScript === true) {
|
|
28733
28738
|
this.byScript.add(entry.index);
|
|
28734
28739
|
}
|
|
28735
|
-
this.lastHeadline[source] = headline ? { detail } : null;
|
|
28740
|
+
this.lastHeadline[source] = headline ? { detail, continues: source === "stderr" && headline.text.endsWith(":") } : null;
|
|
28736
28741
|
}
|
|
28737
28742
|
raisedByScript(entry) {
|
|
28738
28743
|
return this.byScript.has(entry.index);
|
|
@@ -33314,7 +33319,7 @@ var HEADLESS_OPERATIONS = dictionary({
|
|
|
33314
33319
|
uid: "get_uid",
|
|
33315
33320
|
refresh_classes: "refresh_class_cache"
|
|
33316
33321
|
}),
|
|
33317
|
-
project_export: dictionary({ list: "list_export_presets" }),
|
|
33322
|
+
project_export: dictionary({ list: "list_export_presets", add_preset: "add_export_preset" }),
|
|
33318
33323
|
script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
|
|
33319
33324
|
script_info: dictionary({ structure: "get_script_info" }),
|
|
33320
33325
|
editor_classes: dictionary({
|
|
@@ -35290,20 +35295,36 @@ var TOOL_SPECS = [
|
|
|
35290
35295
|
{
|
|
35291
35296
|
name: "project_export",
|
|
35292
35297
|
title: "Export",
|
|
35293
|
-
description: "Export presets and exports.",
|
|
35298
|
+
description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
|
|
35294
35299
|
parameters: {
|
|
35295
35300
|
projectPath: PROJECT_PATH2,
|
|
35296
|
-
preset: {
|
|
35301
|
+
preset: {
|
|
35302
|
+
type: "string",
|
|
35303
|
+
ops: ["run", "add_preset"],
|
|
35304
|
+
description: "Preset name from export_presets.cfg. add_preset: the name of the new one."
|
|
35305
|
+
},
|
|
35306
|
+
platform: {
|
|
35307
|
+
type: "string",
|
|
35308
|
+
ops: ["add_preset"],
|
|
35309
|
+
enum: ["Windows Desktop", "Linux", "macOS", "Android", "iOS", "Web"],
|
|
35310
|
+
description: "add_preset: the platform the preset exports for."
|
|
35311
|
+
},
|
|
35297
35312
|
outputPath: {
|
|
35298
35313
|
type: "string",
|
|
35299
|
-
|
|
35314
|
+
ops: ["run", "add_preset"],
|
|
35315
|
+
description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset. add_preset: the path the preset exports to from the editor, optional.`
|
|
35300
35316
|
},
|
|
35301
|
-
debug: { type: "boolean", description: "run: a debug export. Default false." }
|
|
35317
|
+
debug: { type: "boolean", ops: ["run"], description: "run: a debug export. Default false." }
|
|
35302
35318
|
},
|
|
35303
35319
|
requires: ["projectPath"],
|
|
35304
35320
|
operations: {
|
|
35305
35321
|
list: { effect: "read", summary: "the presets in export_presets.cfg", requires: [] },
|
|
35306
|
-
run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] }
|
|
35322
|
+
run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] },
|
|
35323
|
+
add_preset: {
|
|
35324
|
+
effect: "additive",
|
|
35325
|
+
summary: "add an export preset to export_presets.cfg",
|
|
35326
|
+
requires: ["preset", "platform"]
|
|
35327
|
+
}
|
|
35307
35328
|
}
|
|
35308
35329
|
},
|
|
35309
35330
|
{
|
|
@@ -37565,7 +37586,7 @@ function exportPresetRefusal(projectPath, preset) {
|
|
|
37565
37586
|
try {
|
|
37566
37587
|
text = readFileSync24(join26(projectPath, "export_presets.cfg"), "utf8");
|
|
37567
37588
|
} catch {
|
|
37568
|
-
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with.
|
|
37589
|
+
return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
|
|
37569
37590
|
}
|
|
37570
37591
|
const names = [];
|
|
37571
37592
|
let inPreset = false;
|
|
@@ -37583,9 +37604,9 @@ function exportPresetRefusal(projectPath, preset) {
|
|
|
37583
37604
|
if (names.includes(preset)) {
|
|
37584
37605
|
return null;
|
|
37585
37606
|
}
|
|
37586
|
-
return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with.
|
|
37607
|
+
return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. project_export add_preset adds one.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
|
|
37587
37608
|
}
|
|
37588
|
-
function exportAnswer(ending, asked, before, after) {
|
|
37609
|
+
function exportAnswer(ending, asked, before, after, beside = []) {
|
|
37589
37610
|
const { log } = ending;
|
|
37590
37611
|
const written = after !== null && after !== before;
|
|
37591
37612
|
const notCopied = librariesNotCopied(log.all);
|
|
@@ -37597,6 +37618,7 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
37597
37618
|
failure: ending.failure ?? undefined,
|
|
37598
37619
|
errors: log.count("error"),
|
|
37599
37620
|
warnings: log.count("warning"),
|
|
37621
|
+
...beside.length === 0 ? {} : { alsoWritten: beside },
|
|
37600
37622
|
...notCopied.length === 0 ? {} : {
|
|
37601
37623
|
librariesNotLoaded: notCopied,
|
|
37602
37624
|
extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
|
|
@@ -37619,6 +37641,20 @@ function exportAnswer(ending, asked, before, after) {
|
|
|
37619
37641
|
isError: true
|
|
37620
37642
|
};
|
|
37621
37643
|
}
|
|
37644
|
+
function writtenBeside(before, after, output, within) {
|
|
37645
|
+
return [...after].filter(([name, at]) => name !== output && before.get(name) !== at).map(([name]) => within === "." ? name : `${within}/${name}`).sort();
|
|
37646
|
+
}
|
|
37647
|
+
function filesIn(folder) {
|
|
37648
|
+
const found = new Map;
|
|
37649
|
+
try {
|
|
37650
|
+
for (const entry of readdirSync11(folder, { withFileTypes: true })) {
|
|
37651
|
+
if (entry.isFile()) {
|
|
37652
|
+
found.set(entry.name, statSync12(join26(folder, entry.name)).mtimeMs);
|
|
37653
|
+
}
|
|
37654
|
+
}
|
|
37655
|
+
} catch {}
|
|
37656
|
+
return found;
|
|
37657
|
+
}
|
|
37622
37658
|
function scanWaitAnswer(scanned, started = "game") {
|
|
37623
37659
|
if (scanned.waitedMs === 0) {
|
|
37624
37660
|
return {};
|
|
@@ -37856,6 +37892,9 @@ var PATH_SOLUTIONS = [
|
|
|
37856
37892
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
37857
37893
|
"Point projectPath at the project the file belongs to"
|
|
37858
37894
|
];
|
|
37895
|
+
var OUTPUT_PATH_SOLUTIONS = [
|
|
37896
|
+
'Give outputPath inside the project, relative to it, such as "build/game.exe" or "res://build/game.exe"; copy the build elsewhere once it is exported'
|
|
37897
|
+
];
|
|
37859
37898
|
var PROJECT_FILE_ARGUMENTS = [
|
|
37860
37899
|
"scenePath",
|
|
37861
37900
|
"instancePath",
|
|
@@ -38910,6 +38949,9 @@ class GodotServer {
|
|
|
38910
38949
|
if (tool === "project_import" && op === "set_options") {
|
|
38911
38950
|
return await this.handleSetImportOptions(args);
|
|
38912
38951
|
}
|
|
38952
|
+
if (tool === "project_export" && op === "add_preset") {
|
|
38953
|
+
return await this.handleAddExportPreset(args);
|
|
38954
|
+
}
|
|
38913
38955
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
38914
38956
|
if (headless !== undefined) {
|
|
38915
38957
|
const { from: asked, ...answerable } = args;
|
|
@@ -39973,6 +40015,30 @@ class GodotServer {
|
|
|
39973
40015
|
return this.createErrorResponse(`Search failed: ${errorMessage(error)}`);
|
|
39974
40016
|
}
|
|
39975
40017
|
}
|
|
40018
|
+
async handleAddExportPreset(args) {
|
|
40019
|
+
const project = this.project(args);
|
|
40020
|
+
if (!project.ok) {
|
|
40021
|
+
return project.response;
|
|
40022
|
+
}
|
|
40023
|
+
const given = readNonEmptyString(args, "outputPath");
|
|
40024
|
+
const output = given === undefined ? null : resolveWithinProject(project.value.path, given);
|
|
40025
|
+
if (output !== null && !output.ok) {
|
|
40026
|
+
return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
|
|
40027
|
+
}
|
|
40028
|
+
const outcome = await this.operation("add_export_preset", {
|
|
40029
|
+
preset: readString(args, "preset") ?? "",
|
|
40030
|
+
platform: readString(args, "platform") ?? "",
|
|
40031
|
+
outputPath: output?.relativePath ?? ""
|
|
40032
|
+
}, project.value.path);
|
|
40033
|
+
if (!outcome.ok || !this.editorHasOpen(project.value.path)) {
|
|
40034
|
+
return this.answer(outcome);
|
|
40035
|
+
}
|
|
40036
|
+
return this.jsonTextResponse({
|
|
40037
|
+
...outcome.payload,
|
|
40038
|
+
editorNote: "The editor open on this project read export_presets.cfg when it started and does not list this preset, and saving presets in its Export dialog writes its own list over the file. editor_launch restart loads the file; an export through project_export run reads the file and needs no restart.",
|
|
40039
|
+
...engineExtras(outcome)
|
|
40040
|
+
});
|
|
40041
|
+
}
|
|
39976
40042
|
async handleExportProject(args) {
|
|
39977
40043
|
const project = this.project(args);
|
|
39978
40044
|
if (!project.ok) {
|
|
@@ -39981,11 +40047,14 @@ class GodotServer {
|
|
|
39981
40047
|
const preset = readNonEmptyString(args, "preset") ?? "";
|
|
39982
40048
|
const output = resolveWithinProject(project.value.path, readString(args, "outputPath") ?? "");
|
|
39983
40049
|
if (!output.ok) {
|
|
39984
|
-
return this.createErrorResponse(output.reason,
|
|
40050
|
+
return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
|
|
39985
40051
|
}
|
|
39986
40052
|
const noPreset = exportPresetRefusal(project.value.path, preset);
|
|
39987
40053
|
if (noPreset !== null) {
|
|
39988
|
-
return this.createErrorResponse(noPreset, [
|
|
40054
|
+
return this.createErrorResponse(noPreset, [
|
|
40055
|
+
"project_export list reads the presets the project has",
|
|
40056
|
+
"project_export add_preset adds one"
|
|
40057
|
+
]);
|
|
39989
40058
|
}
|
|
39990
40059
|
const engine = await this.engine();
|
|
39991
40060
|
if (!engine.ok) {
|
|
@@ -40012,6 +40081,7 @@ class GodotServer {
|
|
|
40012
40081
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
40013
40082
|
const writtenAt = () => existsSync20(output.absolutePath) ? statSync12(output.absolutePath).mtimeMs : null;
|
|
40014
40083
|
const before = writtenAt();
|
|
40084
|
+
const listedBefore = filesIn(folder);
|
|
40015
40085
|
const freed = freeLibraryCopies(project.value.path);
|
|
40016
40086
|
if (freed.setAside.length > 0 || freed.swept.length > 0) {
|
|
40017
40087
|
this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
|
|
@@ -40025,7 +40095,8 @@ class GodotServer {
|
|
|
40025
40095
|
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
40026
40096
|
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
40027
40097
|
}
|
|
40028
|
-
|
|
40098
|
+
const beside = writtenBeside(listedBefore, filesIn(folder), basename7(output.absolutePath), dirname18(output.relativePath).replaceAll("\\", "/"));
|
|
40099
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside);
|
|
40029
40100
|
}
|
|
40030
40101
|
reportTestProgress(log) {
|
|
40031
40102
|
const progress = callProgress();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.35",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|