gdharness 1.1.4 → 1.1.6
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 +396 -96
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +64 -15
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +50 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +10 -3
- package/build/godot/operations/godot_operations.gd +0 -2
- package/build/godot/operations/import_pipeline.gd +250 -85
- package/build/index.js +405 -108
- package/build/keeper.js +205 -23
- package/package.json +1 -1
|
@@ -4,12 +4,45 @@ const Read = preload("reading.gd")
|
|
|
4
4
|
const FileWalk = preload("file_walk.gd")
|
|
5
5
|
const Log = preload("logger.gd")
|
|
6
6
|
|
|
7
|
+
## What the engine imports with no tool outside it, for finding what has not been imported yet;
|
|
8
|
+
## everything imported already is found by its sidecar whatever its extension.
|
|
7
9
|
const IMPORTABLE_EXTENSIONS: Array[String] = [
|
|
8
|
-
"png",
|
|
10
|
+
"png",
|
|
11
|
+
"jpg",
|
|
12
|
+
"jpeg",
|
|
13
|
+
"webp",
|
|
14
|
+
"svg",
|
|
15
|
+
"bmp",
|
|
16
|
+
"tga",
|
|
17
|
+
"exr",
|
|
18
|
+
"hdr",
|
|
19
|
+
"dds",
|
|
20
|
+
"ktx",
|
|
21
|
+
"wav",
|
|
22
|
+
"mp3",
|
|
23
|
+
"ogg",
|
|
24
|
+
"ttf",
|
|
25
|
+
"otf",
|
|
26
|
+
"ttc",
|
|
27
|
+
"otc",
|
|
28
|
+
"woff",
|
|
29
|
+
"woff2",
|
|
30
|
+
"pfb",
|
|
31
|
+
"pfm",
|
|
32
|
+
"fnt",
|
|
33
|
+
"font",
|
|
34
|
+
"glb",
|
|
35
|
+
"gltf",
|
|
36
|
+
"fbx",
|
|
37
|
+
"obj",
|
|
38
|
+
"dae",
|
|
9
39
|
]
|
|
40
|
+
const EDITOR_DIRECTORY: String = "res://.godot/editor"
|
|
41
|
+
const EDITOR_CACHE_PREFIX: String = "filesystem_cache"
|
|
10
42
|
|
|
11
43
|
var _log: Log
|
|
12
44
|
var _files: FileWalk = FileWalk.new()
|
|
45
|
+
var _sidecar_md5s: Variant = null
|
|
13
46
|
|
|
14
47
|
|
|
15
48
|
func _init(p_log: Log) -> void:
|
|
@@ -29,7 +62,9 @@ func get_import_status(params: Dictionary) -> Dictionary:
|
|
|
29
62
|
)
|
|
30
63
|
|
|
31
64
|
var resources: Array[Dictionary] = []
|
|
32
|
-
var summary: Dictionary = {
|
|
65
|
+
var summary: Dictionary = {
|
|
66
|
+
"total": 0, "needs_reimport": 0, "failed": 0, "up_to_date": 0, "missing_source": 0
|
|
67
|
+
}
|
|
33
68
|
|
|
34
69
|
if not resource_path.is_empty():
|
|
35
70
|
var full_path: String = resource_path
|
|
@@ -40,7 +75,17 @@ func get_import_status(params: Dictionary) -> Dictionary:
|
|
|
40
75
|
resources.append(status)
|
|
41
76
|
_tally(summary, status)
|
|
42
77
|
else:
|
|
78
|
+
# Sources by extension, for what has never been imported, and sidecars, for what was imported
|
|
79
|
+
# from a type the list does not name and for a sidecar whose source is gone.
|
|
80
|
+
var found: Dictionary[String, bool] = {}
|
|
43
81
|
for res_path: String in _files.find_files_with_extensions("res://", IMPORTABLE_EXTENSIONS):
|
|
82
|
+
found[res_path] = true
|
|
83
|
+
for sidecar: String in _files.find_files_with_extensions("res://", ["import"]):
|
|
84
|
+
found[sidecar.trim_suffix(".import")] = true
|
|
85
|
+
var paths: Array[String] = []
|
|
86
|
+
paths.assign(found.keys())
|
|
87
|
+
paths.sort()
|
|
88
|
+
for res_path: String in paths:
|
|
44
89
|
var status: Dictionary = _import_status_of(res_path, res_path + ".import")
|
|
45
90
|
|
|
46
91
|
if include_up_to_date or status["status"] != "up_to_date":
|
|
@@ -91,7 +136,6 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
91
136
|
resource_path = "res://" + resource_path
|
|
92
137
|
|
|
93
138
|
var options: Dictionary = params.get("options", {})
|
|
94
|
-
var do_reimport: bool = Read.as_bool(params.get("reimport", true), true)
|
|
95
139
|
|
|
96
140
|
_log.info("Setting import options for: " + resource_path)
|
|
97
141
|
|
|
@@ -110,56 +154,28 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
110
154
|
var updated_keys: Array[String] = []
|
|
111
155
|
for key: Variant in options:
|
|
112
156
|
var name: String = str(key)
|
|
113
|
-
config.
|
|
157
|
+
var value: Variant = _as_option(options[key], config.get_value("params", name, null))
|
|
158
|
+
config.set_value("params", name, value)
|
|
114
159
|
updated_keys.append(name)
|
|
115
|
-
_log.debug("Set " + name + " = " + str(
|
|
160
|
+
_log.debug("Set " + name + " = " + str(value))
|
|
116
161
|
|
|
117
162
|
err = config.save(import_file_path)
|
|
118
163
|
if err != OK:
|
|
119
164
|
return _log.failure("Failed to save import file: " + str(err))
|
|
120
165
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
# A headless engine has no importer to run, so the .import file is as far as this goes.
|
|
126
|
-
if do_reimport:
|
|
127
|
-
result["note"] = "Import file updated. Run the editor or use 'reimport_resource' to apply changes."
|
|
128
|
-
|
|
129
|
-
return result
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# Reimport a resource or all resources
|
|
133
|
-
func reimport_resource(params: Dictionary) -> Dictionary:
|
|
134
|
-
var resource_path: String = str(params.get("resource_path", ""))
|
|
135
|
-
var force: bool = Read.as_bool(params.get("force", false))
|
|
136
|
-
|
|
137
|
-
_log.info(
|
|
138
|
-
(
|
|
139
|
-
"Reimporting"
|
|
140
|
-
+ (" resource: " + resource_path if not resource_path.is_empty() else " all modified resources")
|
|
141
|
-
)
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
# A full reimport is editor work; headless can only report what the state is.
|
|
145
|
-
var result: Dictionary = {
|
|
146
|
-
"status": "requested",
|
|
147
|
-
"resource_path": resource_path if not resource_path.is_empty() else "all",
|
|
148
|
-
"force": force,
|
|
149
|
-
"note": "Reimport in headless mode is limited. For full reimport, open the project in the editor."
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if not resource_path.is_empty():
|
|
153
|
-
var full_path: String = resource_path
|
|
154
|
-
if not full_path.begins_with("res://"):
|
|
155
|
-
full_path = "res://" + full_path
|
|
156
|
-
|
|
157
|
-
if not FileAccess.file_exists(full_path):
|
|
158
|
-
return _log.failure("Resource file does not exist: " + full_path)
|
|
166
|
+
# The reimport that applies them is the server's, through the editor or the engine's import.
|
|
167
|
+
return {"resource_path": resource_path, "updated_options": updated_keys}
|
|
159
168
|
|
|
160
|
-
result["current_status"] = _import_status_of(full_path, full_path + ".import")["status"]
|
|
161
169
|
|
|
162
|
-
|
|
170
|
+
## [param given] as the option holds it. Every JSON number arrives as a float, and an enum option
|
|
171
|
+
## such as compress/mode written as 1.0 is kept that way by the import, so a whole number becomes
|
|
172
|
+
## an int unless the option already holds a float.
|
|
173
|
+
static func _as_option(given: Variant, held: Variant) -> Variant:
|
|
174
|
+
if given is float and not held is float:
|
|
175
|
+
var number: float = given
|
|
176
|
+
if number == floorf(number):
|
|
177
|
+
return int(number)
|
|
178
|
+
return given
|
|
163
179
|
|
|
164
180
|
|
|
165
181
|
# List export presets
|
|
@@ -367,7 +383,7 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
|
|
|
367
383
|
return {
|
|
368
384
|
"path": resource_path,
|
|
369
385
|
"status": "missing_source",
|
|
370
|
-
"import_file_exists":
|
|
386
|
+
"import_file_exists": FileAccess.file_exists(import_file_path),
|
|
371
387
|
"source_exists": false
|
|
372
388
|
}
|
|
373
389
|
|
|
@@ -376,6 +392,7 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
|
|
|
376
392
|
return {
|
|
377
393
|
"path": resource_path,
|
|
378
394
|
"status": "needs_reimport",
|
|
395
|
+
"reason": "it has not been imported",
|
|
379
396
|
"import_file_exists": false,
|
|
380
397
|
"source_exists": true
|
|
381
398
|
}
|
|
@@ -384,80 +401,228 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
|
|
|
384
401
|
"path": resource_path, "status": "up_to_date", "import_file_exists": true, "source_exists": true
|
|
385
402
|
}
|
|
386
403
|
|
|
404
|
+
# The checks are the ones EditorFileSystem::_test_for_reimport makes, in its order, so a resource
|
|
405
|
+
# reads as needing an import exactly when the editor's next scan would import it.
|
|
406
|
+
var sidecar: ConfigFile = ConfigFile.new()
|
|
407
|
+
var stale: Dictionary = (
|
|
408
|
+
_stale("its import file cannot be read")
|
|
409
|
+
if sidecar.load(import_file_path) != OK
|
|
410
|
+
else _sidecar_staleness(resource_path, import_file_path, sidecar)
|
|
411
|
+
)
|
|
412
|
+
var kept: bool = str(sidecar.get_value("remap", "importer", "")) in ["keep", "skip"]
|
|
413
|
+
if stale.is_empty() and not kept:
|
|
414
|
+
stale = _import_staleness(resource_path, sidecar)
|
|
415
|
+
status.merge(stale, true)
|
|
416
|
+
return status
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
static func _stale(reason: String) -> Dictionary:
|
|
420
|
+
return {"status": "needs_reimport", "reason": reason}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
## What the editor reads off the sidecar before looking at the import: an answer for a stale or
|
|
424
|
+
## failed resource, empty otherwise.
|
|
425
|
+
func _sidecar_staleness(resource_path: String, import_file_path: String, sidecar: ConfigFile) -> Dictionary:
|
|
426
|
+
# The editor compares the sidecar with the one it last imported from before reading anything in
|
|
427
|
+
# it, so an option set without a reimport is imported on its next scan, and not before.
|
|
428
|
+
var imported_from: String = _imported_sidecar_md5s().get(resource_path, "")
|
|
429
|
+
if not imported_from.is_empty() and FileAccess.get_md5(import_file_path) != imported_from:
|
|
430
|
+
return _stale("its import file changed after it was imported, as setting an option does")
|
|
431
|
+
|
|
432
|
+
# A failed import is never tried again on its own: the editor skips it to avoid a loop of
|
|
433
|
+
# reimports, so it stays failed after its source is fixed until something asks for a reimport.
|
|
434
|
+
var valid: Variant = sidecar.get_value("remap", "valid", true)
|
|
435
|
+
if valid is bool and not valid:
|
|
436
|
+
return {
|
|
437
|
+
"status": "failed",
|
|
438
|
+
"reason": "its last import failed, and the editor does not try a failed import again on its own",
|
|
439
|
+
}
|
|
440
|
+
return {}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
## Whether the import itself is still the one its sidecar and source describe: an answer for a
|
|
444
|
+
## stale resource, empty for a current one.
|
|
445
|
+
static func _import_staleness(resource_path: String, sidecar: ConfigFile) -> Dictionary:
|
|
446
|
+
if not sidecar.has_section_key("remap", "uid"):
|
|
447
|
+
return _stale("its import file has no uid, which the import writes")
|
|
448
|
+
|
|
387
449
|
# The sidecar alone does not say the import is there: deleting the outputs under
|
|
388
450
|
# .godot/imported is the ordinary way to force one, and a resource whose output is gone was
|
|
389
451
|
# answered as current, which is what a reimport is then skipped on.
|
|
390
|
-
var outputs: Array[String] = _outputs_of(
|
|
452
|
+
var outputs: Array[String] = _outputs_of(sidecar)
|
|
391
453
|
var missing: Array[String] = []
|
|
392
454
|
for output: String in outputs:
|
|
393
455
|
if not FileAccess.file_exists(output):
|
|
394
456
|
missing.append(output)
|
|
395
457
|
if not missing.is_empty():
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
458
|
+
var gone: Dictionary = _stale("its imported output is not on disk")
|
|
459
|
+
gone["missing_outputs"] = missing
|
|
460
|
+
return gone
|
|
461
|
+
|
|
462
|
+
var source_file: String = str(sidecar.get_value("deps", "source_file", ""))
|
|
463
|
+
if not source_file.is_empty() and source_file != resource_path:
|
|
464
|
+
return _stale("its import file was written for " + source_file)
|
|
400
465
|
|
|
401
466
|
# By content, as the editor judges it: the hash of the source the import recorded, against the
|
|
402
467
|
# file. By time, an installer that rewrote 31 images byte for byte made every one read as changed,
|
|
403
468
|
# and the editor, which compares the hash, rightly imported none of them.
|
|
404
|
-
var recorded:
|
|
405
|
-
var
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
469
|
+
var recorded: Dictionary = _recorded_md5s(resource_path)
|
|
470
|
+
var recorded_source: String = recorded.get("source_md5", "")
|
|
471
|
+
if recorded_source.is_empty():
|
|
472
|
+
return _stale("its import recorded no hash of its source")
|
|
473
|
+
if FileAccess.get_md5(resource_path) != recorded_source:
|
|
474
|
+
return _stale("the source changed after it was imported")
|
|
475
|
+
|
|
476
|
+
var recorded_outputs: String = recorded.get("dest_md5", "")
|
|
477
|
+
if (
|
|
478
|
+
not outputs.is_empty()
|
|
479
|
+
and not recorded_outputs.is_empty()
|
|
480
|
+
and _md5_of_all(outputs) != recorded_outputs
|
|
481
|
+
):
|
|
482
|
+
return _stale("its imported output changed after it was imported")
|
|
414
483
|
|
|
415
484
|
# A scene built without an image it names: it loads each one as it imports, so one imported
|
|
416
485
|
# before the image was is built without it. Read off what the imported scene depends on, not off
|
|
417
486
|
# file times: a texture a 3D scene uses is reimported compressed after the scene, so its outputs
|
|
418
487
|
# are always the newer, and 33 correct scenes read as stale when judged that way.
|
|
488
|
+
#
|
|
489
|
+
# Followed through the resources the scene depends on, since a post-import script that swaps each
|
|
490
|
+
# material for a saved one leaves the scene depending on the material and only the material on
|
|
491
|
+
# the image: 211 correct scenes read as stale when only the scene's own list was read. And not
|
|
492
|
+
# held against a scene an import script shaped at all, since the script may replace a material
|
|
493
|
+
# with one using other images entirely, which is the scene it was meant to build.
|
|
419
494
|
var without: Array[String] = []
|
|
420
495
|
var images: Array[String] = _images_of(resource_path)
|
|
421
|
-
if not images.is_empty():
|
|
422
|
-
var depended:
|
|
496
|
+
if not images.is_empty() and not _shaped_by_a_script(sidecar):
|
|
497
|
+
var depended: Dictionary[String, bool] = _depended_on(resource_path)
|
|
423
498
|
for image: String in images:
|
|
424
|
-
if FileAccess.file_exists(image + ".import") and not
|
|
499
|
+
if FileAccess.file_exists(image + ".import") and not depended.has(image):
|
|
425
500
|
without.append(image)
|
|
426
|
-
if
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return
|
|
501
|
+
if without.is_empty():
|
|
502
|
+
return {}
|
|
503
|
+
var built: Dictionary = _stale("it was imported without images it uses, which were not imported yet")
|
|
504
|
+
built["imported_without"] = without
|
|
505
|
+
return built
|
|
431
506
|
|
|
432
507
|
|
|
433
|
-
## The
|
|
434
|
-
##
|
|
435
|
-
|
|
508
|
+
## The hashes [param resource_path]'s last import recorded, [code]source_md5[/code] of its source
|
|
509
|
+
## and [code]dest_md5[/code] of its outputs, empty when there is no record: the editor keeps them
|
|
510
|
+
## beside the outputs, under the same base name with [code].md5[/code].
|
|
511
|
+
static func _recorded_md5s(resource_path: String) -> Dictionary:
|
|
436
512
|
var record: String = (
|
|
437
513
|
"res://.godot/imported/%s-%s.md5" % [resource_path.get_file(), resource_path.md5_text()]
|
|
438
514
|
)
|
|
439
515
|
var config: ConfigFile = ConfigFile.new()
|
|
440
516
|
if config.load(record) != OK:
|
|
517
|
+
return {}
|
|
518
|
+
return {
|
|
519
|
+
"source_md5": str(config.get_value("", "source_md5", "")),
|
|
520
|
+
"dest_md5": str(config.get_value("", "dest_md5", "")),
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
## One md5 over the contents of [param paths] in turn, as [code]FileAccess::get_multiple_md5[/code]
|
|
525
|
+
## computes the [code]dest_md5[/code] an import records.
|
|
526
|
+
static func _md5_of_all(paths: Array[String]) -> String:
|
|
527
|
+
var hashing: HashingContext = HashingContext.new()
|
|
528
|
+
if hashing.start(HashingContext.HASH_MD5) != OK:
|
|
441
529
|
return ""
|
|
442
|
-
|
|
530
|
+
for path: String in paths:
|
|
531
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
532
|
+
if file == null:
|
|
533
|
+
continue
|
|
534
|
+
while not file.eof_reached():
|
|
535
|
+
var chunk: PackedByteArray = file.get_buffer(32768)
|
|
536
|
+
if chunk.is_empty():
|
|
537
|
+
break
|
|
538
|
+
if hashing.update(chunk) != OK:
|
|
539
|
+
return ""
|
|
540
|
+
return hashing.finish().hex_encode()
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
## The md5 of each resource's sidecar as the editor last imported it, keyed by path, from the cache
|
|
544
|
+
## the editor writes under .godot/editor; empty when there is no cache or it cannot be read.
|
|
545
|
+
func _imported_sidecar_md5s() -> Dictionary:
|
|
546
|
+
if _sidecar_md5s != null:
|
|
547
|
+
return _sidecar_md5s
|
|
548
|
+
var found: Dictionary = {}
|
|
549
|
+
_sidecar_md5s = found
|
|
550
|
+
var cache: String = _editor_cache_path()
|
|
551
|
+
var file: FileAccess = null if cache.is_empty() else FileAccess.open(cache, FileAccess.READ)
|
|
552
|
+
if file == null:
|
|
553
|
+
return found
|
|
554
|
+
# The first line is the import settings version; then a "::<dir>::<time>" line opens each
|
|
555
|
+
# directory, and each file line is nine fields split by "::", the last holding the file's
|
|
556
|
+
# dependencies, which may contain the splitter. The eighth field is "<>"-separated, with the
|
|
557
|
+
# sidecar's md5 sixth.
|
|
558
|
+
var directory: String = "res://"
|
|
559
|
+
var first: bool = true
|
|
560
|
+
while not file.eof_reached():
|
|
561
|
+
var line: String = file.get_line().strip_edges()
|
|
562
|
+
if first:
|
|
563
|
+
first = false
|
|
564
|
+
continue
|
|
565
|
+
if line.is_empty():
|
|
566
|
+
continue
|
|
567
|
+
if line.begins_with("::"):
|
|
568
|
+
var opened: PackedStringArray = line.split("::")
|
|
569
|
+
if opened.size() == 3:
|
|
570
|
+
directory = opened[1]
|
|
571
|
+
continue
|
|
572
|
+
var fields: PackedStringArray = line.split("::", true, 8)
|
|
573
|
+
if fields.size() < 9:
|
|
574
|
+
continue
|
|
575
|
+
var slices: PackedStringArray = fields[7].split("<>")
|
|
576
|
+
if slices.size() >= 7 and not slices[5].is_empty():
|
|
577
|
+
found[directory.path_join(fields[0])] = slices[5]
|
|
578
|
+
return found
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
## The editor's filesystem cache, the newest version of it when an engine upgrade has left an older
|
|
582
|
+
## one beside it, or "" when the project has none. Its name carries a format version that the
|
|
583
|
+
## engine raises when the layout changes: 4.7.2 writes filesystem_cache10.
|
|
584
|
+
static func _editor_cache_path() -> String:
|
|
585
|
+
var newest: int = -1
|
|
586
|
+
if not DirAccess.dir_exists_absolute(EDITOR_DIRECTORY):
|
|
587
|
+
return ""
|
|
588
|
+
for name: String in DirAccess.get_files_at(EDITOR_DIRECTORY):
|
|
589
|
+
if name.begins_with(EDITOR_CACHE_PREFIX) and name.trim_prefix(EDITOR_CACHE_PREFIX).is_valid_int():
|
|
590
|
+
newest = maxi(newest, name.trim_prefix(EDITOR_CACHE_PREFIX).to_int())
|
|
591
|
+
return "" if newest < 0 else EDITOR_DIRECTORY.path_join(EDITOR_CACHE_PREFIX + str(newest))
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
## The path of one entry [method ResourceLoader.get_dependencies] lists, past any uid and type.
|
|
595
|
+
static func _path_of(entry: String) -> String:
|
|
596
|
+
var at: int = entry.rfind("::")
|
|
597
|
+
return entry if at < 0 else entry.substr(at + 2)
|
|
443
598
|
|
|
444
599
|
|
|
445
|
-
##
|
|
446
|
-
##
|
|
447
|
-
static func
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
600
|
+
## Every path [param resource_path] depends on, through the resources it depends on in turn; a set,
|
|
601
|
+
## keyed by path.
|
|
602
|
+
static func _depended_on(resource_path: String) -> Dictionary[String, bool]:
|
|
603
|
+
var found: Dictionary[String, bool] = {}
|
|
604
|
+
var pending: Array[String] = [resource_path]
|
|
605
|
+
# Every path is followed once, which ends the walk and survives a cycle; a cap or a list of
|
|
606
|
+
# extensions would leave out a material behind a long chain or in a saved .mesh.
|
|
607
|
+
while not pending.is_empty():
|
|
608
|
+
var next: String = pending.pop_back()
|
|
609
|
+
for entry: String in ResourceLoader.get_dependencies(next):
|
|
610
|
+
var path: String = _path_of(entry)
|
|
611
|
+
if not found.has(path):
|
|
612
|
+
found[path] = true
|
|
613
|
+
pending.append(path)
|
|
614
|
+
return found
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
## Whether the import [param sidecar] describes runs a post-import script.
|
|
618
|
+
static func _shaped_by_a_script(sidecar: ConfigFile) -> bool:
|
|
619
|
+
return not str(sidecar.get_value("params", "import_script/path", "")).is_empty()
|
|
452
620
|
|
|
453
621
|
|
|
454
622
|
## The files an import wrote, from its sidecar's [code]dest_files[/code]; empty when it lists none.
|
|
455
|
-
static func _outputs_of(
|
|
456
|
-
var config: ConfigFile = ConfigFile.new()
|
|
623
|
+
static func _outputs_of(sidecar: ConfigFile) -> Array[String]:
|
|
457
624
|
var outputs: Array[String] = []
|
|
458
|
-
|
|
459
|
-
return outputs
|
|
460
|
-
var listed: Variant = config.get_value("deps", "dest_files", [])
|
|
625
|
+
var listed: Variant = sidecar.get_value("deps", "dest_files", [])
|
|
461
626
|
if listed is Array:
|
|
462
627
|
for one: Variant in listed:
|
|
463
628
|
outputs.append(str(one))
|