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/errors.rb
CHANGED
|
@@ -17,9 +17,9 @@ module Ukiryu
|
|
|
17
17
|
class UnknownShellError < Error
|
|
18
18
|
def suggestions
|
|
19
19
|
[
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
'Supported shells: bash, zsh, fish, sh, dash, tcsh, powershell, cmd',
|
|
21
|
+
'Platform groups: :unix (all Unix shells), :windows, :powershell',
|
|
22
|
+
'Set explicitly: Ukiryu.configure { |c| c.default_shell = :bash }'
|
|
23
23
|
]
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -28,9 +28,9 @@ module Ukiryu
|
|
|
28
28
|
class UnsupportedPlatformError < Error
|
|
29
29
|
def suggestions
|
|
30
30
|
[
|
|
31
|
-
|
|
31
|
+
'Ukiryu supports: macOS, Linux, Windows',
|
|
32
32
|
"Current platform: #{RUBY_PLATFORM}",
|
|
33
|
-
|
|
33
|
+
'Check if running on a supported operating system'
|
|
34
34
|
]
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -39,9 +39,9 @@ module Ukiryu
|
|
|
39
39
|
class ValidationError < Error
|
|
40
40
|
def suggestions
|
|
41
41
|
[
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
'Check the parameter type against the tool definition',
|
|
43
|
+
'Verify value is within allowed range',
|
|
44
|
+
'Ensure value is in the allowed values list'
|
|
45
45
|
]
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -50,9 +50,9 @@ module Ukiryu
|
|
|
50
50
|
class ProfileNotFoundError < Error
|
|
51
51
|
def suggestions
|
|
52
52
|
[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
'Check tool definition has profile for your platform',
|
|
54
|
+
'Verify tool definition has profile for your shell',
|
|
55
|
+
'Try specifying platform/shell explicitly'
|
|
56
56
|
]
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -60,9 +60,9 @@ module Ukiryu
|
|
|
60
60
|
class ProfileLoadError < Error
|
|
61
61
|
def suggestions
|
|
62
62
|
[
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
'Verify YAML syntax is correct',
|
|
64
|
+
'Check profile structure matches schema',
|
|
65
|
+
'Review error message for specific issue'
|
|
66
66
|
]
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -73,9 +73,9 @@ module Ukiryu
|
|
|
73
73
|
class DefinitionNotFoundError < DefinitionError
|
|
74
74
|
def suggestions
|
|
75
75
|
[
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
'Verify file path is correct',
|
|
77
|
+
'Check file has .yaml extension',
|
|
78
|
+
'Use absolute path if relative path fails'
|
|
79
79
|
]
|
|
80
80
|
end
|
|
81
81
|
end
|
|
@@ -83,9 +83,9 @@ module Ukiryu
|
|
|
83
83
|
class DefinitionLoadError < DefinitionError
|
|
84
84
|
def suggestions
|
|
85
85
|
[
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
'Validate YAML syntax',
|
|
87
|
+
'Check file is readable',
|
|
88
|
+
'Verify file encoding is UTF-8'
|
|
89
89
|
]
|
|
90
90
|
end
|
|
91
91
|
end
|
|
@@ -94,8 +94,8 @@ module Ukiryu
|
|
|
94
94
|
def suggestions
|
|
95
95
|
[
|
|
96
96
|
"Run 'ukiryu validate' for detailed errors",
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
'Compare with schema definition',
|
|
98
|
+
'Check tool definition examples'
|
|
99
99
|
]
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -107,9 +107,9 @@ module Ukiryu
|
|
|
107
107
|
class ToolNotFoundError < Error
|
|
108
108
|
def suggestions
|
|
109
109
|
[
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
'Check tool name spelling',
|
|
111
|
+
'Verify register path is correct',
|
|
112
|
+
'List available tools: Ukiryu::Register.tool_names'
|
|
113
113
|
]
|
|
114
114
|
end
|
|
115
115
|
end
|
|
@@ -117,9 +117,9 @@ module Ukiryu
|
|
|
117
117
|
class ExecutableNotFoundError < Error
|
|
118
118
|
def suggestions
|
|
119
119
|
[
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
'Install the tool (e.g., brew install imagemagick)',
|
|
121
|
+
'Add executable to PATH',
|
|
122
|
+
'Configure search_paths in tool definition'
|
|
123
123
|
]
|
|
124
124
|
end
|
|
125
125
|
end
|
|
@@ -128,9 +128,9 @@ module Ukiryu
|
|
|
128
128
|
class ExecutionError < Error
|
|
129
129
|
def suggestions
|
|
130
130
|
[
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
'Check e.result.exit_status for exit code',
|
|
132
|
+
'Check e.result.stderr for error message',
|
|
133
|
+
'Verify parameters are correct'
|
|
134
134
|
]
|
|
135
135
|
end
|
|
136
136
|
end
|
|
@@ -145,9 +145,9 @@ module Ukiryu
|
|
|
145
145
|
|
|
146
146
|
def suggestions
|
|
147
147
|
[
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
'Increase timeout parameter',
|
|
149
|
+
'Check UKIRYU_TIMEOUT environment variable',
|
|
150
|
+
'Verify tool is not hanging'
|
|
151
151
|
]
|
|
152
152
|
end
|
|
153
153
|
end
|
|
@@ -156,9 +156,9 @@ module Ukiryu
|
|
|
156
156
|
class VersionDetectionError < Error
|
|
157
157
|
def suggestions
|
|
158
158
|
[
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
'Verify tool is installed correctly',
|
|
160
|
+
'Check version_detection command in tool definition',
|
|
161
|
+
'Test version command manually: tool --version'
|
|
162
162
|
]
|
|
163
163
|
end
|
|
164
164
|
end
|
|
@@ -50,23 +50,17 @@ module Ukiryu
|
|
|
50
50
|
|
|
51
51
|
# Try primary name first
|
|
52
52
|
result = DiscoveryStrategy.discover(tool_name, context)
|
|
53
|
-
if result && (ENV['UKIRYU_DEBUG_EXECUTABLE'] || (platform == :windows && ENV['CI']))
|
|
54
|
-
warn "[UKIRYU DEBUG ExecutableLocator] Found #{tool_name}: #{result[:path]}"
|
|
55
|
-
end
|
|
53
|
+
warn "[UKIRYU DEBUG ExecutableLocator] Found #{tool_name}: #{result[:path]}" if result && (ENV['UKIRYU_DEBUG_EXECUTABLE'] || (platform == :windows && ENV['CI']))
|
|
56
54
|
return result if result
|
|
57
55
|
|
|
58
56
|
# Try aliases
|
|
59
57
|
aliases.each do |alias_name|
|
|
60
58
|
result = DiscoveryStrategy.discover(alias_name, context)
|
|
61
|
-
if result && (ENV['UKIRYU_DEBUG_EXECUTABLE'] || (platform == :windows && ENV['CI']))
|
|
62
|
-
warn "[UKIRYU DEBUG ExecutableLocator] Found alias #{alias_name}: #{result[:path]}"
|
|
63
|
-
end
|
|
59
|
+
warn "[UKIRYU DEBUG ExecutableLocator] Found alias #{alias_name}: #{result[:path]}" if result && (ENV['UKIRYU_DEBUG_EXECUTABLE'] || (platform == :windows && ENV['CI']))
|
|
64
60
|
return result if result
|
|
65
61
|
end
|
|
66
62
|
|
|
67
|
-
if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (platform == :windows && ENV['CI'])
|
|
68
|
-
warn "[UKIRYU DEBUG ExecutableLocator] NO EXECUTABLE FOUND for #{tool_name} or aliases #{aliases}"
|
|
69
|
-
end
|
|
63
|
+
warn "[UKIRYU DEBUG ExecutableLocator] NO EXECUTABLE FOUND for #{tool_name} or aliases #{aliases}" if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (platform == :windows && ENV['CI'])
|
|
70
64
|
|
|
71
65
|
nil
|
|
72
66
|
end
|
|
@@ -151,9 +145,7 @@ module Ukiryu
|
|
|
151
145
|
# @param context [DiscoveryContext] discovery environment
|
|
152
146
|
# @return [Hash, nil] discovery result or nil
|
|
153
147
|
def discover(command, context)
|
|
154
|
-
if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (context.platform == :windows && ENV['CI'])
|
|
155
|
-
warn "[UKIRYU DEBUG AliasDiscovery] Checking for alias: #{command.inspect} with shell #{context.shell_class}"
|
|
156
|
-
end
|
|
148
|
+
warn "[UKIRYU DEBUG AliasDiscovery] Checking for alias: #{command.inspect} with shell #{context.shell_class}" if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (context.platform == :windows && ENV['CI'])
|
|
157
149
|
|
|
158
150
|
alias_info = context.shell_class.detect_alias(command)
|
|
159
151
|
warn "[UKIRYU DEBUG AliasDiscovery] Alias info: #{alias_info.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (context.platform == :windows && ENV['CI'])
|
|
@@ -162,9 +154,7 @@ module Ukiryu
|
|
|
162
154
|
alias_target = alias_info[:target]
|
|
163
155
|
path = PathScanner.find(command) || PathScanner.find(alias_target)
|
|
164
156
|
|
|
165
|
-
if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (context.platform == :windows && ENV['CI'])
|
|
166
|
-
warn "[UKIRYU DEBUG AliasDiscovery] Alias target: #{alias_target.inspect}, path: #{path.inspect}"
|
|
167
|
-
end
|
|
157
|
+
warn "[UKIRYU DEBUG AliasDiscovery] Alias target: #{alias_target.inspect}, path: #{path.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (context.platform == :windows && ENV['CI'])
|
|
168
158
|
|
|
169
159
|
DiscoveryResult.build(path, :alias, context, alias_info[:definition]) if path
|
|
170
160
|
end
|
|
@@ -325,12 +315,21 @@ module Ukiryu
|
|
|
325
315
|
|
|
326
316
|
# Handle platform-specific path extensions (.exe, .bat, etc.)
|
|
327
317
|
#
|
|
318
|
+
# On Windows, prioritizes .exe over .com for better PowerShell compatibility.
|
|
319
|
+
# The .com extension is legacy and can cause issues with PowerShell's call
|
|
320
|
+
# operator when used with I/O redirection (hangs with Open3.capture3).
|
|
321
|
+
#
|
|
328
322
|
# @api private
|
|
329
323
|
class PathExtensions
|
|
330
324
|
include Enumerable
|
|
331
325
|
|
|
326
|
+
# Extensions to prioritize on Windows for PowerShell compatibility
|
|
327
|
+
# .com files can hang PowerShell when used with I/O redirection
|
|
328
|
+
PREFERRED_WINDOWS_EXTENSIONS = %w[.exe .EXE].freeze
|
|
329
|
+
|
|
332
330
|
def initialize
|
|
333
|
-
|
|
331
|
+
raw_extensions = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
|
332
|
+
@extensions = prioritize_extensions(raw_extensions)
|
|
334
333
|
end
|
|
335
334
|
|
|
336
335
|
# Iterate over extensions
|
|
@@ -339,6 +338,31 @@ module Ukiryu
|
|
|
339
338
|
def each(&block)
|
|
340
339
|
@extensions.each(&block)
|
|
341
340
|
end
|
|
341
|
+
|
|
342
|
+
private
|
|
343
|
+
|
|
344
|
+
# Prioritize .exe extensions on Windows for better PowerShell compatibility
|
|
345
|
+
#
|
|
346
|
+
# @param extensions [Array<String>] original PATHEXT extensions
|
|
347
|
+
# @return [Array<String>] reordered extensions with .exe first
|
|
348
|
+
def prioritize_extensions(extensions)
|
|
349
|
+
return extensions unless Platform.windows?
|
|
350
|
+
|
|
351
|
+
# Separate preferred extensions from others
|
|
352
|
+
preferred = []
|
|
353
|
+
others = []
|
|
354
|
+
|
|
355
|
+
extensions.each do |ext|
|
|
356
|
+
if PREFERRED_WINDOWS_EXTENSIONS.include?(ext)
|
|
357
|
+
preferred << ext
|
|
358
|
+
else
|
|
359
|
+
others << ext
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Return preferred first, then others in original order
|
|
364
|
+
preferred + others
|
|
365
|
+
end
|
|
342
366
|
end
|
|
343
367
|
end
|
|
344
368
|
end
|
|
@@ -59,7 +59,8 @@ module Ukiryu
|
|
|
59
59
|
args = command[1..]
|
|
60
60
|
|
|
61
61
|
# Execute through Executor (uses Environment system internally)
|
|
62
|
-
result = Ukiryu::Executor.execute(executable, args, env: environment, shell: shell_class, allow_failure: true,
|
|
62
|
+
result = Ukiryu::Executor.execute(executable, args, env: environment, shell: shell_class, allow_failure: true,
|
|
63
|
+
timeout: timeout)
|
|
63
64
|
|
|
64
65
|
{
|
|
65
66
|
stdout: result.stdout,
|
|
@@ -212,6 +212,9 @@ module Ukiryu
|
|
|
212
212
|
/is not recognized as (?:a |the )?(?:name of a )?cmdlet/i,
|
|
213
213
|
/cannot be found/i,
|
|
214
214
|
/because it does not exist/i,
|
|
215
|
+
# Windows Start-Process error when executable not found
|
|
216
|
+
/The system cannot find the file specified/i,
|
|
217
|
+
/cannot find the file/i,
|
|
215
218
|
# General
|
|
216
219
|
/executable not found/i,
|
|
217
220
|
/bad command or file name/i
|
data/lib/ukiryu/logger.rb
CHANGED
|
@@ -10,12 +10,17 @@ module Ukiryu
|
|
|
10
10
|
# - Colored output via Paint gem (when available)
|
|
11
11
|
# - Message classification (debug, info, warn, error)
|
|
12
12
|
# - Structured output for tool resolution process
|
|
13
|
+
# - Class-level convenience methods for quick debug output
|
|
13
14
|
#
|
|
14
15
|
# @example Enable debug mode
|
|
15
16
|
# ENV['UKIRYU_DEBUG'] = '1'
|
|
16
17
|
# logger = Ukiryu::Logger.new
|
|
17
18
|
# logger.debug("Tool resolution started")
|
|
18
19
|
#
|
|
20
|
+
# @example Class-level debug output (recommended)
|
|
21
|
+
# Ukiryu::Logger.debug("Message", category: :executable)
|
|
22
|
+
# Ukiryu::Logger.debug("Found executable", context: { path: '/usr/bin/tool' })
|
|
23
|
+
#
|
|
19
24
|
# @example Standard logging
|
|
20
25
|
# logger = Ukiryu::Logger.new
|
|
21
26
|
# logger.info("Command completed successfully")
|
|
@@ -25,6 +30,52 @@ module Ukiryu
|
|
|
25
30
|
# Log levels
|
|
26
31
|
LEVELS = %i[debug info warn error].freeze
|
|
27
32
|
|
|
33
|
+
# Environment variable names for debug control
|
|
34
|
+
DEBUG_ENV_VAR = 'UKIRYU_DEBUG'
|
|
35
|
+
DEBUG_EXECUTABLE_ENV_VAR = 'UKIRYU_DEBUG_EXECUTABLE'
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Class-level debug output (quick access without instance)
|
|
39
|
+
#
|
|
40
|
+
# @param message [String] the debug message
|
|
41
|
+
# @param category [Symbol, nil] optional category (:executable for UKIRYU_DEBUG_EXECUTABLE)
|
|
42
|
+
# @param context [Hash] optional context data to include
|
|
43
|
+
def debug(message, category: nil, context: {})
|
|
44
|
+
return unless debug_enabled?(category)
|
|
45
|
+
|
|
46
|
+
prefix = "[UKIRYU DEBUG#{category ? " #{category.to_s.upcase}" : ''}]"
|
|
47
|
+
details = context.empty? ? '' : " (#{context.map { |k, v| "#{k}=#{v.inspect}" }.join(', ')})"
|
|
48
|
+
warn "#{prefix} #{message}#{details}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if debug mode is enabled for a given category
|
|
52
|
+
#
|
|
53
|
+
# @param category [Symbol, nil] the category to check
|
|
54
|
+
# @return [Boolean] true if debug is enabled
|
|
55
|
+
def debug_enabled?(category = nil)
|
|
56
|
+
case category
|
|
57
|
+
when :executable
|
|
58
|
+
ENV[DEBUG_EXECUTABLE_ENV_VAR] || (Platform.windows? && ENV['CI'])
|
|
59
|
+
else
|
|
60
|
+
ENV[DEBUG_ENV_VAR]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the singleton instance
|
|
65
|
+
#
|
|
66
|
+
# @return [Logger] the logger instance
|
|
67
|
+
def instance
|
|
68
|
+
@instance ||= new
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Delegate instance methods to class level
|
|
72
|
+
%i[info warn error debug_resolution].each do |method|
|
|
73
|
+
define_method(method) do |*args, **kwargs|
|
|
74
|
+
instance.send(method, *args, **kwargs)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
28
79
|
attr_reader :level, :logger, :output, :paint_available
|
|
29
80
|
|
|
30
81
|
# Initialize a new Logger
|
|
@@ -66,7 +66,8 @@ module Ukiryu
|
|
|
66
66
|
# @return [ImplementationIndex] Loaded index
|
|
67
67
|
def self.from_yaml(path)
|
|
68
68
|
require 'psych'
|
|
69
|
-
data = Psych.safe_load_file(path,
|
|
69
|
+
data = Psych.safe_load_file(path,
|
|
70
|
+
permitted_classes: [Symbol, String, Integer, Array, Hash, TrueClass, FalseClass])
|
|
70
71
|
from_hash(data)
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -50,6 +50,17 @@ module Ukiryu
|
|
|
50
50
|
platforms.include?(platform_str) && shells.include?(shell_str)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Debug output for profile selection
|
|
54
|
+
if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (ENV['CI'] && defined?(Ukiryu::Platform) && Ukiryu::Platform.windows?)
|
|
55
|
+
warn "[UKIRYU DEBUG compatible_profile] platform=#{platform_str}, shell=#{shell_str}"
|
|
56
|
+
warn "[UKIRYU DEBUG compatible_profile] Found profile: #{profile ? (profile[:name] || profile['name']) : 'nil'}"
|
|
57
|
+
if profile
|
|
58
|
+
warn "[UKIRYU DEBUG compatible_profile] Profile inherits: #{profile[:inherits] || profile['inherits']}"
|
|
59
|
+
warn "[UKIRYU DEBUG compatible_profile] Profile commands nil?: #{profile[:commands].nil?}"
|
|
60
|
+
warn "[UKIRYU DEBUG compatible_profile] Profile commands empty?: #{(profile[:commands] || []).empty?}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
53
64
|
return nil unless profile
|
|
54
65
|
|
|
55
66
|
# Resolve profile inheritance at hash level
|
|
@@ -61,11 +72,16 @@ module Ukiryu
|
|
|
61
72
|
prof_name.to_s == inherits.to_s
|
|
62
73
|
end
|
|
63
74
|
|
|
75
|
+
if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (ENV['CI'] && defined?(Ukiryu::Platform) && Ukiryu::Platform.windows?)
|
|
76
|
+
warn "[UKIRYU DEBUG compatible_profile] Looking for parent '#{inherits}': #{parent_profile ? 'found' : 'not found'}"
|
|
77
|
+
end
|
|
78
|
+
|
|
64
79
|
if parent_profile && (profile[:commands].nil? || profile[:commands].empty?)
|
|
65
80
|
# Copy parent's commands to child profile (without modifying original)
|
|
66
81
|
parent_commands = parent_profile[:commands] || parent_profile['commands']
|
|
67
82
|
# Return a new hash with inherited commands
|
|
68
83
|
profile = profile.dup.merge(commands: parent_commands)
|
|
84
|
+
warn "[UKIRYU DEBUG compatible_profile] Inherited #{parent_commands&.size || 0} commands from parent" if ENV['UKIRYU_DEBUG_EXECUTABLE'] || (ENV['CI'] && defined?(Ukiryu::Platform) && Ukiryu::Platform.windows?)
|
|
69
85
|
end
|
|
70
86
|
end
|
|
71
87
|
|
|
@@ -86,7 +102,8 @@ module Ukiryu
|
|
|
86
102
|
# @return [ImplementationVersion] Loaded version
|
|
87
103
|
def self.from_yaml(path)
|
|
88
104
|
require 'psych'
|
|
89
|
-
data = Psych.safe_load_file(path,
|
|
105
|
+
data = Psych.safe_load_file(path,
|
|
106
|
+
permitted_classes: [Symbol, String, Integer, Array, Hash, TrueClass, FalseClass])
|
|
90
107
|
from_hash(data)
|
|
91
108
|
end
|
|
92
109
|
|
|
@@ -63,7 +63,8 @@ module Ukiryu
|
|
|
63
63
|
# @return [Interface] Loaded interface
|
|
64
64
|
def self.from_yaml(path)
|
|
65
65
|
require 'psych'
|
|
66
|
-
data = Psych.safe_load_file(path,
|
|
66
|
+
data = Psych.safe_load_file(path,
|
|
67
|
+
permitted_classes: [Symbol, String, Integer, Array, Hash, TrueClass, FalseClass])
|
|
67
68
|
from_hash(symbolize_keys(data))
|
|
68
69
|
end
|
|
69
70
|
|
|
@@ -5,7 +5,6 @@ require 'etc'
|
|
|
5
5
|
|
|
6
6
|
module Ukiryu
|
|
7
7
|
module Models
|
|
8
|
-
|
|
9
8
|
# Run environment information
|
|
10
9
|
class RunEnvironment < Lutaml::Model::Serializable
|
|
11
10
|
attribute :hostname, :string, default: ''
|
|
@@ -107,6 +106,5 @@ module Ukiryu
|
|
|
107
106
|
|
|
108
107
|
private_class_method :detect_shell_version_for, :detect_total_memory, :os_version_string
|
|
109
108
|
end
|
|
110
|
-
|
|
111
109
|
end
|
|
112
110
|
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
module Models
|
|
5
|
+
# Semantic version value object for proper version comparison.
|
|
6
|
+
#
|
|
7
|
+
# Handles version strings like "10.0", "9.5.1", "1.2.3" and provides
|
|
8
|
+
# proper semantic comparison (10.0 > 9.5, not alphabetical).
|
|
9
|
+
#
|
|
10
|
+
# This class ensures that version selection uses actual semantic meaning
|
|
11
|
+
# rather than string comparison, which would incorrectly sort "9.5" > "10.0".
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# v1 = Ukiryu::Models::SemanticVersion.new("10.0")
|
|
15
|
+
# v2 = Ukiryu::Models::SemanticVersion.new("9.5")
|
|
16
|
+
# v1 > v2 # => true (correct)
|
|
17
|
+
# "10.0" > "9.5" # => false (wrong - alphabetical)
|
|
18
|
+
#
|
|
19
|
+
class SemanticVersion
|
|
20
|
+
include Comparable
|
|
21
|
+
|
|
22
|
+
# Parse a version string into segments
|
|
23
|
+
#
|
|
24
|
+
# @param version_string [String, nil] the version string to parse
|
|
25
|
+
# @return [Array<Integer>] array of numeric segments
|
|
26
|
+
def self.parse(version_string)
|
|
27
|
+
return [0] if version_string.nil? || version_string.to_s.empty?
|
|
28
|
+
|
|
29
|
+
version_string.to_s
|
|
30
|
+
.split('.')
|
|
31
|
+
.map do |part|
|
|
32
|
+
part.to_i
|
|
33
|
+
rescue StandardError
|
|
34
|
+
0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Compare two version strings directly
|
|
39
|
+
#
|
|
40
|
+
# @param version_a [String] first version string
|
|
41
|
+
# @param version_b [String] second version string
|
|
42
|
+
# @return [Integer] -1, 0, or 1
|
|
43
|
+
def self.compare(version_a, version_b)
|
|
44
|
+
segments1 = parse(version_a)
|
|
45
|
+
segments2 = parse(version_b)
|
|
46
|
+
|
|
47
|
+
max_length = [segments1.length, segments2.length].max
|
|
48
|
+
padded1 = segments1 + [0] * (max_length - segments1.length)
|
|
49
|
+
padded2 = segments2 + [0] * (max_length - segments2.length)
|
|
50
|
+
|
|
51
|
+
padded1 <=> padded2
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Array<Integer>] the numeric segments of this version
|
|
55
|
+
attr_reader :segments
|
|
56
|
+
|
|
57
|
+
# @return [String, nil] the original version string
|
|
58
|
+
attr_reader :original
|
|
59
|
+
|
|
60
|
+
# Create a new SemanticVersion from a version string
|
|
61
|
+
#
|
|
62
|
+
# @param version_string [String, Integer, SemanticVersion, nil] the version
|
|
63
|
+
def initialize(version_string)
|
|
64
|
+
@original = version_string.respond_to?(:to_s) ? version_string.to_s : nil
|
|
65
|
+
@segments = self.class.parse(@original)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Compare this version with another
|
|
69
|
+
#
|
|
70
|
+
# @param other [SemanticVersion, String, Integer, nil] the other version
|
|
71
|
+
# @return [Integer, nil] -1, 0, 1, or nil if not comparable
|
|
72
|
+
def <=>(other)
|
|
73
|
+
return nil unless other
|
|
74
|
+
|
|
75
|
+
other_segments = case other
|
|
76
|
+
when SemanticVersion
|
|
77
|
+
other.segments
|
|
78
|
+
when String, Integer
|
|
79
|
+
self.class.parse(other)
|
|
80
|
+
else
|
|
81
|
+
return nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Compare segment by segment
|
|
85
|
+
# [10, 0] <=> [9, 5] should return 1 (10.0 > 9.5)
|
|
86
|
+
max_length = [segments.length, other_segments.length].max
|
|
87
|
+
|
|
88
|
+
max_length.times do |i|
|
|
89
|
+
a = segments[i] || 0
|
|
90
|
+
b = other_segments[i] || 0
|
|
91
|
+
|
|
92
|
+
return a <=> b unless a == b
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
0 # All segments equal
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check equality with another version
|
|
99
|
+
#
|
|
100
|
+
# @param other [Object] the other object
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def ==(other)
|
|
103
|
+
return false unless other
|
|
104
|
+
|
|
105
|
+
(self <=> other).zero?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if this version is greater than another
|
|
109
|
+
#
|
|
110
|
+
# @param other [SemanticVersion, String, Integer, nil] the other version
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def >(other)
|
|
113
|
+
(self <=> other) == 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check if this version is less than another
|
|
117
|
+
#
|
|
118
|
+
# @param other [SemanticVersion, String, Integer, nil] the other version
|
|
119
|
+
# @return [Boolean]
|
|
120
|
+
def <(other)
|
|
121
|
+
(self <=> other) == -1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if this version is greater than or equal to another
|
|
125
|
+
#
|
|
126
|
+
# @param other [SemanticVersion, String, Integer, nil] the other version
|
|
127
|
+
# @return [Boolean]
|
|
128
|
+
def >=(other)
|
|
129
|
+
result = self <=> other
|
|
130
|
+
[1, 0].include?(result)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if this version is less than or equal to another
|
|
134
|
+
#
|
|
135
|
+
# @param other [SemanticVersion, String, Integer, nil] the other version
|
|
136
|
+
# @return [Boolean]
|
|
137
|
+
def <=(other)
|
|
138
|
+
result = self <=> other
|
|
139
|
+
[-1, 0].include?(result)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Return the version as a string
|
|
143
|
+
#
|
|
144
|
+
# @return [String] the version string
|
|
145
|
+
def to_s
|
|
146
|
+
segments.join('.')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Return a human-readable representation
|
|
150
|
+
#
|
|
151
|
+
# @return [String]
|
|
152
|
+
def inspect
|
|
153
|
+
"#<Ukiryu::Models::SemanticVersion #{self}>"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Hash for use as hash key
|
|
157
|
+
#
|
|
158
|
+
# @return [Integer]
|
|
159
|
+
def hash
|
|
160
|
+
segments.hash
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Equality for hash key usage
|
|
164
|
+
#
|
|
165
|
+
# @param other [Object]
|
|
166
|
+
# @return [Boolean]
|
|
167
|
+
def eql?(other)
|
|
168
|
+
return false unless other.is_a?(SemanticVersion)
|
|
169
|
+
|
|
170
|
+
segments == other.segments
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|