ukiryu 0.1.6 → 0.2.0
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.
- checksums.yaml +4 -4
- data/lib/ukiryu/cache.rb +6 -0
- data/lib/ukiryu/cache_registry.rb +64 -0
- data/lib/ukiryu/cli_commands/base_command.rb +6 -5
- data/lib/ukiryu/cli_commands/config_command.rb +7 -10
- data/lib/ukiryu/cli_commands/register_command.rb +27 -18
- data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
- data/lib/ukiryu/command_builder.rb +83 -50
- data/lib/ukiryu/config.rb +13 -2
- data/lib/ukiryu/debug.rb +20 -9
- data/lib/ukiryu/definition/loader.rb +3 -3
- data/lib/ukiryu/errors.rb +37 -37
- data/lib/ukiryu/executable_locator.rb +40 -16
- data/lib/ukiryu/extractors/base_extractor.rb +2 -1
- data/lib/ukiryu/extractors/help_parser.rb +3 -0
- data/lib/ukiryu/logger.rb +51 -0
- data/lib/ukiryu/models/implementation_index.rb +2 -1
- data/lib/ukiryu/models/implementation_version.rb +18 -1
- data/lib/ukiryu/models/interface.rb +2 -1
- data/lib/ukiryu/models/run_environment.rb +0 -2
- data/lib/ukiryu/models/semantic_version.rb +174 -0
- data/lib/ukiryu/models/stage_metrics.rb +0 -1
- data/lib/ukiryu/register.rb +473 -232
- data/lib/ukiryu/shell/powershell.rb +209 -89
- data/lib/ukiryu/shell/sh.rb +4 -1
- data/lib/ukiryu/shell.rb +60 -2
- data/lib/ukiryu/tool/command_resolution.rb +2 -1
- data/lib/ukiryu/tool/executable_discovery.rb +14 -15
- data/lib/ukiryu/tool/loader.rb +543 -0
- data/lib/ukiryu/tool/version_detection.rb +1 -3
- data/lib/ukiryu/tool.rb +79 -87
- data/lib/ukiryu/tool_index.rb +127 -62
- data/lib/ukiryu/tools/base.rb +4 -2
- data/lib/ukiryu/type.rb +26 -15
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu.rb +1 -1
- data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
- data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
- data/spec/spec_helper.rb +10 -6
- data/spec/support/tool_helper.rb +2 -0
- data/spec/ukiryu/definition/loader_spec.rb +2 -2
- data/spec/ukiryu/executor_spec.rb +6 -3
- data/spec/ukiryu/models/execution_report_spec.rb +3 -2
- data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
- data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
- data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
- data/spec/ukiryu/shell/powershell_spec.rb +286 -51
- data/spec/ukiryu/tool/loader_spec.rb +148 -0
- data/spec/ukiryu/tool_index_spec.rb +110 -18
- data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
- data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
- data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
- metadata +14 -2
- data/lib/ukiryu/register_auto_manager.rb +0 -342
data/lib/ukiryu/tool.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'tool_cache'
|
|
4
4
|
require_relative 'tool_finder'
|
|
5
|
+
require_relative 'tool/loader'
|
|
5
6
|
|
|
6
7
|
module Ukiryu
|
|
7
8
|
# Tool wrapper class for external command-line tools
|
|
@@ -44,48 +45,13 @@ module Ukiryu
|
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
# Try loading a tool using the new ImplementationIndex architecture
|
|
47
|
-
#
|
|
48
|
+
# Delegates to Tool::Loader module
|
|
48
49
|
#
|
|
49
50
|
# @param name [String, Symbol] the tool name
|
|
50
51
|
# @param options [Hash] loading options
|
|
51
52
|
# @return [Tool, nil] the tool instance or nil if not using new architecture
|
|
52
53
|
def load_with_implementation_index(name, options = {})
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# Try to load ImplementationIndex
|
|
56
|
-
index = Register.load_implementation_index(name, options)
|
|
57
|
-
return nil unless index
|
|
58
|
-
|
|
59
|
-
# Load Interface
|
|
60
|
-
interface = Register.load_interface(index.interface, options)
|
|
61
|
-
return nil unless interface
|
|
62
|
-
|
|
63
|
-
# Detect implementation and version
|
|
64
|
-
impl_spec = detect_implementation_and_version(index, name, options)
|
|
65
|
-
return nil unless impl_spec
|
|
66
|
-
|
|
67
|
-
# Load ImplementationVersion
|
|
68
|
-
impl_version = Register.load_implementation_version(
|
|
69
|
-
name,
|
|
70
|
-
impl_spec[:implementation_name],
|
|
71
|
-
impl_spec[:file],
|
|
72
|
-
options
|
|
73
|
-
)
|
|
74
|
-
return nil unless impl_version
|
|
75
|
-
|
|
76
|
-
# Convert to old ToolDefinition format for compatibility
|
|
77
|
-
profile = convert_to_tool_definition(
|
|
78
|
-
name,
|
|
79
|
-
interface,
|
|
80
|
-
impl_version,
|
|
81
|
-
impl_spec[:implementation_name],
|
|
82
|
-
impl_spec[:version], # Pass detected version
|
|
83
|
-
options
|
|
84
|
-
)
|
|
85
|
-
return nil unless profile
|
|
86
|
-
|
|
87
|
-
# Create tool instance
|
|
88
|
-
new(profile, options)
|
|
54
|
+
Loader.load_with_implementation_index(name, options)
|
|
89
55
|
end
|
|
90
56
|
|
|
91
57
|
# Detect implementation and version from ImplementationIndex
|
|
@@ -129,11 +95,13 @@ module Ukiryu
|
|
|
129
95
|
# Prefer implementation-level default, then version-level default, then last version
|
|
130
96
|
impl_default = impl[:default] || impl['default']
|
|
131
97
|
version_spec = if impl_default
|
|
132
|
-
|
|
133
|
-
|
|
98
|
+
# Find version spec matching the implementation default
|
|
99
|
+
versions.find do |v|
|
|
100
|
+
v[:file] == impl_default || v['file'] == impl_default
|
|
101
|
+
end || versions.last
|
|
134
102
|
else
|
|
135
|
-
|
|
136
|
-
|
|
103
|
+
versions.find { |v| v[:default] || v['default'] } || versions.last
|
|
104
|
+
end
|
|
137
105
|
return {
|
|
138
106
|
implementation_name: impl[:name] || impl['name'],
|
|
139
107
|
version: nil,
|
|
@@ -162,11 +130,11 @@ module Ukiryu
|
|
|
162
130
|
# Prefer implementation-level default, then version-level default, then last version
|
|
163
131
|
impl_default = impl[:default] || impl['default']
|
|
164
132
|
default_spec = if impl_default
|
|
165
|
-
|
|
166
|
-
|
|
133
|
+
# Find version spec matching the implementation default
|
|
134
|
+
versions.find { |v| v[:file] == impl_default || v['file'] == impl_default } || versions.last
|
|
167
135
|
else
|
|
168
|
-
|
|
169
|
-
|
|
136
|
+
versions.find { |v| v[:default] || v['default'] } || versions.last
|
|
137
|
+
end
|
|
170
138
|
{
|
|
171
139
|
implementation_name: impl[:name] || impl['name'],
|
|
172
140
|
version: nil,
|
|
@@ -260,14 +228,14 @@ module Ukiryu
|
|
|
260
228
|
|
|
261
229
|
versions.each do |version_spec|
|
|
262
230
|
range_type = if version_spec[:equals]
|
|
263
|
-
|
|
231
|
+
:equals
|
|
264
232
|
elsif version_spec[:before]
|
|
265
|
-
|
|
233
|
+
:before
|
|
266
234
|
elsif version_spec[:after]
|
|
267
|
-
|
|
235
|
+
:after
|
|
268
236
|
else
|
|
269
|
-
|
|
270
|
-
end
|
|
237
|
+
version_spec[:between] ? :between : nil
|
|
238
|
+
end
|
|
271
239
|
|
|
272
240
|
next unless range_type
|
|
273
241
|
|
|
@@ -301,7 +269,8 @@ end
|
|
|
301
269
|
# @param implementation_name [String] implementation name
|
|
302
270
|
# @param options [Hash] options
|
|
303
271
|
# @return [ToolDefinition] converted tool definition
|
|
304
|
-
def convert_to_tool_definition(tool_name, interface, impl_version, implementation_name, detected_version,
|
|
272
|
+
def convert_to_tool_definition(tool_name, interface, impl_version, implementation_name, detected_version,
|
|
273
|
+
options = {})
|
|
305
274
|
require_relative 'models/tool_definition'
|
|
306
275
|
require_relative 'models/platform_profile'
|
|
307
276
|
|
|
@@ -316,13 +285,13 @@ end
|
|
|
316
285
|
# Build ToolDefinition from execution profile
|
|
317
286
|
# Note: implements must be an array for the v2 model
|
|
318
287
|
# Only append implementation name for non-default implementations
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
288
|
+
if implementation_name && implementation_name != 'default'
|
|
289
|
+
"#{tool_name}_#{implementation_name}"
|
|
290
|
+
else
|
|
291
|
+
tool_name
|
|
292
|
+
end
|
|
324
293
|
# Use detected version if available, otherwise fall back to YAML version
|
|
325
|
-
|
|
294
|
+
detected_version || impl_version.version
|
|
326
295
|
# Build ToolDefinition from execution profile
|
|
327
296
|
# Note: implements must be an array for the v2 model
|
|
328
297
|
# Only append implementation name for non-default implementations
|
|
@@ -377,11 +346,11 @@ end
|
|
|
377
346
|
# @return [Hash] profile hash
|
|
378
347
|
def convert_profile_to_hash(profile, actions)
|
|
379
348
|
# Handle both Hash and ExecutionProfile objects
|
|
349
|
+
actions_hash = actions || {}
|
|
350
|
+
commands_array = convert_actions_to_array(actions_hash)
|
|
380
351
|
if profile.is_a?(Hash)
|
|
381
352
|
# Use the actions parameter (interface.actions), not profile[:actions]
|
|
382
|
-
actions_hash = actions || {}
|
|
383
353
|
# Convert actions hash to array format expected by ToolDefinition
|
|
384
|
-
commands_array = convert_actions_to_array(actions_hash)
|
|
385
354
|
{
|
|
386
355
|
'name' => profile[:name] || profile['name'],
|
|
387
356
|
'display_name' => profile[:display_name] || profile['display_name'],
|
|
@@ -392,8 +361,6 @@ end
|
|
|
392
361
|
'commands' => commands_array
|
|
393
362
|
}
|
|
394
363
|
else
|
|
395
|
-
actions_hash = actions || {}
|
|
396
|
-
commands_array = convert_actions_to_array(actions_hash)
|
|
397
364
|
{
|
|
398
365
|
'name' => profile.name,
|
|
399
366
|
'display_name' => profile.display_name,
|
|
@@ -441,25 +408,25 @@ end
|
|
|
441
408
|
# If profile has commands, merge them with interface actions
|
|
442
409
|
# If profile has no commands, use interface actions directly
|
|
443
410
|
command_definitions = if profile_commands.nil? || profile_commands.empty?
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
411
|
+
# No profile commands - use interface actions directly
|
|
412
|
+
interface_commands_hash.map do |_cmd_name, cmd_hash|
|
|
413
|
+
convert_hash_to_command_definition(cmd_hash)
|
|
414
|
+
end
|
|
448
415
|
else
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
416
|
+
# Profile has commands - merge with interface actions
|
|
417
|
+
profile_commands.map do |cmd_hash|
|
|
418
|
+
# Command name may be specified as 'name' or 'subcommand' field
|
|
419
|
+
cmd_name = cmd_hash[:name] || cmd_hash['name'] || cmd_hash[:subcommand] || cmd_hash['subcommand']
|
|
420
|
+
# Merge profile command data with interface action data
|
|
421
|
+
interface_cmd = interface_commands_hash[cmd_name]
|
|
422
|
+
merged_cmd_hash = if interface_cmd
|
|
423
|
+
# Deep merge: profile data takes precedence
|
|
424
|
+
deep_merge_hashes(interface_cmd, cmd_hash)
|
|
425
|
+
else
|
|
426
|
+
cmd_hash
|
|
427
|
+
end
|
|
428
|
+
convert_hash_to_command_definition(merged_cmd_hash)
|
|
429
|
+
end
|
|
463
430
|
end
|
|
464
431
|
|
|
465
432
|
# Create PlatformProfile
|
|
@@ -501,7 +468,7 @@ end
|
|
|
501
468
|
warn "[UKIRYU DEBUG build_command_definition] cmd.name: #{cmd_hash['name'] || cmd_hash[:name]}"
|
|
502
469
|
warn "[UKIRYU DEBUG build_command_definition] post_options_data: #{post_options_data.inspect}"
|
|
503
470
|
warn "[UKIRYU DEBUG build_command_definition] post_options_data.class: #{post_options_data.class}" if post_options_data
|
|
504
|
-
if post_options_data
|
|
471
|
+
if post_options_data.is_a?(Array)
|
|
505
472
|
post_options_data.first(2).each do |opt|
|
|
506
473
|
warn "[UKIRYU DEBUG build_command_definition] post_option: #{opt.inspect}"
|
|
507
474
|
end
|
|
@@ -563,7 +530,8 @@ end
|
|
|
563
530
|
when 'inputs' then 'arguments'
|
|
564
531
|
else nested_key.to_s
|
|
565
532
|
end
|
|
566
|
-
command_def[target_key.to_sym] = nested_value unless [:signature,
|
|
533
|
+
command_def[target_key.to_sym] = nested_value unless [:signature,
|
|
534
|
+
'signature'].include?(nested_key)
|
|
567
535
|
end
|
|
568
536
|
else
|
|
569
537
|
command_def[key] = value
|
|
@@ -587,6 +555,7 @@ end
|
|
|
587
555
|
# @option options [String] :register_path path to tool profiles
|
|
588
556
|
# @option options [Symbol] :platform platform to use
|
|
589
557
|
# @option options [Symbol] :shell shell to use
|
|
558
|
+
# @raise [Ukiryu::Errors::ToolNotFoundError] if tool is not found
|
|
590
559
|
# @return [Tool] the tool instance
|
|
591
560
|
def get(name, options = {})
|
|
592
561
|
# Check cache first
|
|
@@ -602,6 +571,31 @@ end
|
|
|
602
571
|
tool
|
|
603
572
|
end
|
|
604
573
|
|
|
574
|
+
# Find a tool by name, returning nil if not found.
|
|
575
|
+
#
|
|
576
|
+
# This is a non-raising alternative to {.get} for cases where
|
|
577
|
+
# tool absence is expected and should be handled gracefully.
|
|
578
|
+
#
|
|
579
|
+
# @param name [String, Symbol] the tool name
|
|
580
|
+
# @param options [Hash] initialization options
|
|
581
|
+
# @option options [String] :register_path path to tool profiles
|
|
582
|
+
# @option options [Symbol] :platform platform to use
|
|
583
|
+
# @option options [Symbol] :shell shell to use
|
|
584
|
+
# @return [Tool, nil] the tool instance or nil if not found
|
|
585
|
+
#
|
|
586
|
+
# @example
|
|
587
|
+
# tool = Ukiryu::Tool.find(:imagemagick)
|
|
588
|
+
# if tool
|
|
589
|
+
# tool.execute(:convert, inputs: ["image.png"])
|
|
590
|
+
# else
|
|
591
|
+
# puts "ImageMagick not available"
|
|
592
|
+
# end
|
|
593
|
+
def find(name, options = {})
|
|
594
|
+
get(name, options)
|
|
595
|
+
rescue Ukiryu::Errors::ToolNotFoundError
|
|
596
|
+
nil
|
|
597
|
+
end
|
|
598
|
+
|
|
605
599
|
# Find a tool by name, alias, or interface
|
|
606
600
|
#
|
|
607
601
|
# Searches for a tool that matches the given identifier by:
|
|
@@ -1000,9 +994,7 @@ end
|
|
|
1000
994
|
# Normalize params to hash with symbol keys
|
|
1001
995
|
params = normalize_params(params)
|
|
1002
996
|
|
|
1003
|
-
if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
1004
|
-
warn "[UKIRYU DEBUG execute_simple] params (after normalize): #{params.inspect}"
|
|
1005
|
-
end
|
|
997
|
+
warn "[UKIRYU DEBUG execute_simple] params (after normalize): #{params.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
1006
998
|
|
|
1007
999
|
# Extract stdin parameter if present (special parameter, not passed to command)
|
|
1008
1000
|
stdin = params.delete(:stdin)
|
|
@@ -1034,10 +1026,10 @@ end
|
|
|
1034
1026
|
# Use command-specific executable if profile explicitly allows it
|
|
1035
1027
|
# This is determined by checking if the command has standalone_executable: true
|
|
1036
1028
|
allows_standalone = if command.respond_to?(:standalone_executable?)
|
|
1037
|
-
|
|
1029
|
+
command.standalone_executable?
|
|
1038
1030
|
else
|
|
1039
|
-
|
|
1040
|
-
|
|
1031
|
+
false
|
|
1032
|
+
end
|
|
1041
1033
|
|
|
1042
1034
|
same_dir_as_exec = allows_standalone &&
|
|
1043
1035
|
File.executable?(exe_path) &&
|
data/lib/ukiryu/tool_index.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Lazy load ToolMetadata only when needed
|
|
4
4
|
autoload :ToolMetadata, File.expand_path('models/tool_metadata', __dir__)
|
|
5
5
|
|
|
6
|
+
require_relative 'models/semantic_version'
|
|
7
|
+
|
|
6
8
|
module Ukiryu
|
|
7
9
|
# Index for fast tool lookup by interface and alias
|
|
8
10
|
#
|
|
@@ -12,20 +14,27 @@ module Ukiryu
|
|
|
12
14
|
# - Register change detection via mtime
|
|
13
15
|
#
|
|
14
16
|
# Built once and cached for the lifetime of the process.
|
|
17
|
+
# Thread-safe for concurrent access.
|
|
15
18
|
#
|
|
16
19
|
# @api private
|
|
17
20
|
class ToolIndex
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
|
|
18
23
|
class << self
|
|
19
|
-
# Get the singleton instance
|
|
24
|
+
# Get the singleton instance (thread-safe)
|
|
20
25
|
#
|
|
21
26
|
# @return [ToolIndex] the index instance
|
|
22
27
|
def instance
|
|
23
|
-
@
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
@instance ||= new
|
|
30
|
+
end
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
# Reset the index (mainly for testing)
|
|
27
34
|
def reset
|
|
28
|
-
@
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@instance = nil
|
|
37
|
+
end
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
# Get all tools in the index (class method delegating to instance)
|
|
@@ -41,13 +50,13 @@ module Ukiryu
|
|
|
41
50
|
# @param register_path [String, nil] the path to the tool register
|
|
42
51
|
def initialize(register_path: nil)
|
|
43
52
|
@register_path = register_path || Ukiryu::Register.default_register_path
|
|
53
|
+
@mutex = Mutex.new
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
55
|
+
Logger.debug('ToolIndex#initialize called', category: :executable)
|
|
56
|
+
Logger.debug("param register_path = #{register_path.inspect}", category: :executable)
|
|
57
|
+
Logger.debug("Ukiryu::Register.default_register_path = #{Ukiryu::Register.default_register_path.inspect}",
|
|
58
|
+
category: :executable)
|
|
59
|
+
Logger.debug("@register_path = #{@register_path.inspect}", category: :executable)
|
|
51
60
|
|
|
52
61
|
@interface_to_tools = {} # interface => [tool_names]
|
|
53
62
|
@alias_to_tool = {} # alias => [tool_names] (multiple tools can share an alias)
|
|
@@ -64,16 +73,18 @@ module Ukiryu
|
|
|
64
73
|
def find_by_interface(interface_name)
|
|
65
74
|
build_index_if_needed
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
tool_names = @interface_to_tools[interface_name]
|
|
78
|
+
return nil unless tool_names
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
# Try each tool implementing this interface until we find one that loads
|
|
81
|
+
tool_names.each do |tool_name|
|
|
82
|
+
metadata = load_metadata_for_tool(tool_name)
|
|
83
|
+
return metadata if metadata
|
|
84
|
+
end
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
# Find all tools that implement an interface
|
|
@@ -83,7 +94,9 @@ module Ukiryu
|
|
|
83
94
|
def find_all_by_interface(interface_name)
|
|
84
95
|
build_index_if_needed
|
|
85
96
|
|
|
86
|
-
@
|
|
97
|
+
@mutex.synchronize do
|
|
98
|
+
@interface_to_tools[interface_name] || []
|
|
99
|
+
end
|
|
87
100
|
end
|
|
88
101
|
|
|
89
102
|
# Find tool by alias
|
|
@@ -96,20 +109,22 @@ module Ukiryu
|
|
|
96
109
|
def find_by_alias(alias_name)
|
|
97
110
|
build_index_if_needed
|
|
98
111
|
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
candidates = @alias_to_tool[alias_name.to_sym]
|
|
114
|
+
return nil unless candidates
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
# If only one tool has this alias, return it directly
|
|
117
|
+
return candidates.first if candidates.one?
|
|
104
118
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
# Multiple tools have this alias - select by platform compatibility
|
|
120
|
+
runtime = Ukiryu::Runtime.instance
|
|
121
|
+
platform = runtime.platform
|
|
122
|
+
shell = runtime.shell
|
|
109
123
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
candidates.find do |tool_name|
|
|
125
|
+
tool_compatible?(tool_name, platform: platform, shell: shell)
|
|
126
|
+
end || candidates.first
|
|
127
|
+
end
|
|
113
128
|
end
|
|
114
129
|
|
|
115
130
|
# Check if a tool is compatible with the given platform and shell
|
|
@@ -165,31 +180,45 @@ module Ukiryu
|
|
|
165
180
|
#
|
|
166
181
|
# @return [Boolean] true if rebuild is needed
|
|
167
182
|
def stale?
|
|
168
|
-
|
|
183
|
+
@mutex.synchronize do
|
|
184
|
+
return true unless @built
|
|
169
185
|
|
|
170
|
-
|
|
171
|
-
|
|
186
|
+
current_cache_key = build_cache_key
|
|
187
|
+
@cache_key != current_cache_key
|
|
188
|
+
end
|
|
172
189
|
end
|
|
173
190
|
|
|
174
191
|
# Update the register path
|
|
175
192
|
#
|
|
176
193
|
# @param new_path [String] the new register path
|
|
177
194
|
def register_path=(new_path)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
@mutex.synchronize do
|
|
196
|
+
return if @register_path == new_path
|
|
197
|
+
|
|
198
|
+
@register_path = new_path
|
|
199
|
+
@built = false # Rebuild index with new path
|
|
200
|
+
@cache_key = nil
|
|
201
|
+
@interface_to_tools = {}
|
|
202
|
+
@alias_to_tool = {}
|
|
203
|
+
@compatibility_cache = {}
|
|
204
|
+
end
|
|
186
205
|
end
|
|
187
206
|
|
|
188
207
|
private
|
|
189
208
|
|
|
190
|
-
# Build index only if needed (lazy loading)
|
|
209
|
+
# Build index only if needed (lazy loading) - thread-safe
|
|
191
210
|
def build_index_if_needed
|
|
192
|
-
|
|
211
|
+
@mutex.synchronize do
|
|
212
|
+
build_index if stale_without_lock?
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Check if stale without acquiring lock (must be called within synchronized block)
|
|
217
|
+
def stale_without_lock?
|
|
218
|
+
return true unless @built
|
|
219
|
+
|
|
220
|
+
current_cache_key = build_cache_key
|
|
221
|
+
@cache_key != current_cache_key
|
|
193
222
|
end
|
|
194
223
|
|
|
195
224
|
# Build cache key for register change detection
|
|
@@ -216,12 +245,11 @@ module Ukiryu
|
|
|
216
245
|
def register_path
|
|
217
246
|
path = @register_path || Ukiryu::Register.default_register_path
|
|
218
247
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
end
|
|
248
|
+
Logger.debug('ToolIndex#register_path called', category: :executable)
|
|
249
|
+
Logger.debug("@register_path = #{@register_path.inspect}", category: :executable)
|
|
250
|
+
Logger.debug("Ukiryu::Register.default_register_path = #{Ukiryu::Register.default_register_path.inspect}",
|
|
251
|
+
category: :executable)
|
|
252
|
+
Logger.debug("returning = #{path.inspect}", category: :executable)
|
|
225
253
|
|
|
226
254
|
path
|
|
227
255
|
end
|
|
@@ -231,10 +259,8 @@ module Ukiryu
|
|
|
231
259
|
def build_index
|
|
232
260
|
current_path = register_path
|
|
233
261
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
warn "[UKIRYU DEBUG] current_path = #{current_path.inspect}"
|
|
237
|
-
end
|
|
262
|
+
Logger.debug('ToolIndex#build_index called', category: :executable)
|
|
263
|
+
Logger.debug("current_path = #{current_path.inspect}", category: :executable)
|
|
238
264
|
|
|
239
265
|
return unless current_path
|
|
240
266
|
|
|
@@ -328,15 +354,17 @@ module Ukiryu
|
|
|
328
354
|
|
|
329
355
|
# Load YAML content for a specific tool
|
|
330
356
|
#
|
|
357
|
+
# Version selection is based on the `version:` field inside YAML content,
|
|
358
|
+
# NOT the filename. This ensures the content is the source of truth.
|
|
359
|
+
#
|
|
331
360
|
# @param tool_name [Symbol, String] the tool name
|
|
332
361
|
# @return [String, nil] the YAML content
|
|
333
362
|
def load_yaml_for_tool(tool_name)
|
|
334
363
|
current_path = register_path
|
|
335
364
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
end
|
|
365
|
+
Logger.debug("ToolIndex#load_yaml_for_tool tool_name=#{tool_name}",
|
|
366
|
+
category: :executable)
|
|
367
|
+
Logger.debug("current_path = #{current_path.inspect}", category: :executable)
|
|
340
368
|
|
|
341
369
|
return nil unless current_path
|
|
342
370
|
|
|
@@ -354,19 +382,56 @@ module Ukiryu
|
|
|
354
382
|
# Prefer 'default' variant if present
|
|
355
383
|
next unless File.basename(variant_dir) == 'default'
|
|
356
384
|
|
|
357
|
-
files = Dir.glob(File.join(variant_dir, '*.yaml'))
|
|
358
|
-
return
|
|
385
|
+
files = Dir.glob(File.join(variant_dir, '*.yaml'))
|
|
386
|
+
return select_latest_version_by_content(files) if files.any?
|
|
359
387
|
|
|
360
388
|
# Fall back to any variant directory
|
|
361
|
-
files = Dir.glob(File.join(variant_dir, '*.yaml'))
|
|
362
|
-
return
|
|
389
|
+
files = Dir.glob(File.join(variant_dir, '*.yaml'))
|
|
390
|
+
return select_latest_version_by_content(files) if files.any?
|
|
363
391
|
end
|
|
364
392
|
|
|
365
393
|
# Fallback: direct YAML files in tool directory (legacy structure)
|
|
366
|
-
files = Dir.glob(File.join(tool_dir, '*.yaml'))
|
|
367
|
-
files.
|
|
394
|
+
files = Dir.glob(File.join(tool_dir, '*.yaml'))
|
|
395
|
+
files.any? ? select_latest_version_by_content(files) : nil
|
|
368
396
|
rescue StandardError
|
|
369
397
|
nil
|
|
370
398
|
end
|
|
399
|
+
|
|
400
|
+
# Select the latest version file by reading version from YAML CONTENT.
|
|
401
|
+
#
|
|
402
|
+
# This is the correct approach: version comes from the `version:` field
|
|
403
|
+
# inside the YAML file, NOT from the filename. The content is the
|
|
404
|
+
# source of truth.
|
|
405
|
+
#
|
|
406
|
+
# @param files [Array<String>] list of YAML file paths
|
|
407
|
+
# @return [String, nil] content of the file with the highest version
|
|
408
|
+
def select_latest_version_by_content(files)
|
|
409
|
+
return nil if files.empty?
|
|
410
|
+
return File.read(files.first) if files.size == 1
|
|
411
|
+
|
|
412
|
+
# Build a map of version (from content) => file content
|
|
413
|
+
versions_to_content = {}
|
|
414
|
+
|
|
415
|
+
files.each do |file|
|
|
416
|
+
content = File.read(file)
|
|
417
|
+
hash = YAML.safe_load(content, permitted_classes: [Symbol], aliases: true)
|
|
418
|
+
next unless hash
|
|
419
|
+
|
|
420
|
+
version_string = hash['version']
|
|
421
|
+
next unless version_string
|
|
422
|
+
|
|
423
|
+
version = Models::SemanticVersion.new(version_string)
|
|
424
|
+
versions_to_content[version] = content
|
|
425
|
+
rescue StandardError => e
|
|
426
|
+
# Skip files that can't be parsed
|
|
427
|
+
Logger.warn("Failed to parse version from #{file}: #{e.message}")
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Return content of the highest version
|
|
431
|
+
return nil if versions_to_content.empty?
|
|
432
|
+
|
|
433
|
+
latest_version = versions_to_content.keys.max
|
|
434
|
+
versions_to_content[latest_version]
|
|
435
|
+
end
|
|
371
436
|
end
|
|
372
437
|
end
|
data/lib/ukiryu/tools/base.rb
CHANGED
|
@@ -259,7 +259,8 @@ module Ukiryu
|
|
|
259
259
|
args = build_args(command_def, params)
|
|
260
260
|
|
|
261
261
|
# Find executable using ExecutableFinder
|
|
262
|
-
executable = Ukiryu::ExecutableFinder.find_executable(self.class.tool_definition.name.to_s,
|
|
262
|
+
executable = Ukiryu::ExecutableFinder.find_executable(self.class.tool_definition.name.to_s,
|
|
263
|
+
self.class.tool_definition)
|
|
263
264
|
|
|
264
265
|
# Get shell
|
|
265
266
|
shell_sym = Ukiryu::Runtime.instance.shell
|
|
@@ -425,7 +426,8 @@ module Ukiryu
|
|
|
425
426
|
|
|
426
427
|
cmd = vd.command
|
|
427
428
|
|
|
428
|
-
executable = Ukiryu::ExecutableFinder.find_executable(self.class.tool_definition.name.to_s,
|
|
429
|
+
executable = Ukiryu::ExecutableFinder.find_executable(self.class.tool_definition.name.to_s,
|
|
430
|
+
self.class.tool_definition)
|
|
429
431
|
return nil unless executable
|
|
430
432
|
|
|
431
433
|
shell_sym = Ukiryu::Runtime.instance.shell
|