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/register.rb
CHANGED
|
@@ -1,109 +1,153 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
|
+
require 'git'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'pathname'
|
|
4
7
|
require_relative 'utils'
|
|
5
8
|
require_relative 'models/interface'
|
|
6
9
|
require_relative 'models/implementation_index'
|
|
7
10
|
require_relative 'models/implementation_version'
|
|
8
|
-
require_relative 'register_auto_manager'
|
|
9
11
|
|
|
10
12
|
module Ukiryu
|
|
11
|
-
#
|
|
13
|
+
# Represents a collection of tool definitions
|
|
12
14
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
+
# A Register is a directory containing tool profiles in YAML format.
|
|
16
|
+
# It handles:
|
|
17
|
+
# - Discovery (env variable, dev path, user clone)
|
|
18
|
+
# - Auto-cloning from GitHub
|
|
19
|
+
# - Loading tool definitions
|
|
20
|
+
# - Validation
|
|
21
|
+
#
|
|
22
|
+
# @example Getting the default register
|
|
23
|
+
# register = Ukiryu::Register.default
|
|
24
|
+
# register.tool_names # => ['ghostscript', 'imagemagick', ...]
|
|
25
|
+
#
|
|
26
|
+
# @example Using a specific path
|
|
27
|
+
# register = Ukiryu::Register.at('/path/to/register')
|
|
28
|
+
#
|
|
29
|
+
# @api private - This is an internal class. Developers use Tool.get()
|
|
15
30
|
#
|
|
16
|
-
# Features:
|
|
17
|
-
# - Cached version listings to avoid repeated glob operations
|
|
18
|
-
# - Automatic cache invalidation when register path changes
|
|
19
|
-
# - Automatic register cloning to ~/.ukiryu/register if not configured
|
|
20
31
|
class Register
|
|
32
|
+
# GitHub repository URL for the register
|
|
33
|
+
REMOTE_URL = 'https://github.com/ukiryu/register'
|
|
34
|
+
|
|
35
|
+
# Default local directory for user clone
|
|
36
|
+
DEFAULT_USER_PATH = '~/.ukiryu/register'
|
|
37
|
+
|
|
38
|
+
# @return [String] the filesystem path to the register
|
|
39
|
+
attr_reader :path
|
|
40
|
+
|
|
41
|
+
# @return [Symbol] how the register was discovered (:env, :dev, :user, :manual)
|
|
42
|
+
attr_reader :source
|
|
43
|
+
|
|
44
|
+
# Error raised when register operations fail
|
|
45
|
+
class Error < StandardError; end
|
|
46
|
+
|
|
47
|
+
# Error raised when register cannot be found or cloned
|
|
48
|
+
class NotFoundError < Error; end
|
|
49
|
+
|
|
50
|
+
# Error raised when cloning fails
|
|
51
|
+
class CloneError < Error; end
|
|
52
|
+
|
|
21
53
|
class << self
|
|
22
|
-
#
|
|
54
|
+
# Get the default register instance
|
|
55
|
+
#
|
|
56
|
+
# Auto-discovers the register using this priority:
|
|
57
|
+
# 1. UKIRYU_REGISTER environment variable
|
|
58
|
+
# 2. Development register (sibling to gem source)
|
|
59
|
+
# 3. User clone at ~/.ukiryu/register (auto-clones if needed)
|
|
23
60
|
#
|
|
24
|
-
# @
|
|
61
|
+
# @return [Register] the default register instance
|
|
62
|
+
def default
|
|
63
|
+
@default ||= discover
|
|
64
|
+
end
|
|
25
65
|
|
|
26
|
-
#
|
|
66
|
+
# Create a register at a specific path
|
|
27
67
|
#
|
|
28
|
-
# @
|
|
29
|
-
|
|
68
|
+
# @param path [String] the filesystem path
|
|
69
|
+
# @return [Register] a new register instance
|
|
70
|
+
def at(path)
|
|
71
|
+
new(File.expand_path(path), source: :manual)
|
|
72
|
+
end
|
|
30
73
|
|
|
31
|
-
# Reset the
|
|
32
|
-
def
|
|
33
|
-
@
|
|
34
|
-
@register_cache_key = nil
|
|
74
|
+
# Reset the cached default register (mainly for testing)
|
|
75
|
+
def reset_default
|
|
76
|
+
@default = nil
|
|
35
77
|
end
|
|
36
78
|
|
|
37
|
-
#
|
|
38
|
-
# Only returns tools that have an index.yaml (new architecture)
|
|
79
|
+
# Check if a default register exists (without auto-cloning)
|
|
39
80
|
#
|
|
40
|
-
# @return [
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
81
|
+
# @return [Boolean] true if a register can be found
|
|
82
|
+
def exists?
|
|
83
|
+
path = resolve_path_without_clone
|
|
84
|
+
path && Dir.exist?(path) && valid_structure?(path)
|
|
85
|
+
end
|
|
44
86
|
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
# ===== BACKWARD COMPATIBILITY =====
|
|
88
|
+
# These class methods delegate to the default instance for backward compatibility
|
|
47
89
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
end.sort
|
|
90
|
+
# @deprecated Use Register.default.path instead
|
|
91
|
+
def default_register_path
|
|
92
|
+
default.path
|
|
52
93
|
end
|
|
53
94
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
95
|
+
# @deprecated Use ENV['UKIRYU_REGISTER'] = path; Register.reset_default instead
|
|
96
|
+
def default_register_path=(path)
|
|
97
|
+
ENV['UKIRYU_REGISTER'] = path
|
|
98
|
+
reset_default
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @deprecated Use Register.reset_default instead
|
|
102
|
+
def reset_version_cache
|
|
103
|
+
reset_default
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @deprecated Use Register.default.tool_names instead
|
|
107
|
+
def tools
|
|
108
|
+
default.tool_names
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @deprecated Use Register.default.list_versions instead
|
|
59
112
|
def list_versions(name, register_path: nil)
|
|
60
|
-
register_path
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# For new architecture, load the index and get versions
|
|
64
|
-
index = load_implementation_index(name, register_path: register_path)
|
|
65
|
-
return {} unless index
|
|
66
|
-
|
|
67
|
-
# Extract versions from all implementations
|
|
68
|
-
# Return a hash mapping version strings to full file paths
|
|
69
|
-
versions = {}
|
|
70
|
-
index.implementations.each do |impl|
|
|
71
|
-
impl_name = impl[:name] || impl['name']
|
|
72
|
-
impl_versions = impl[:versions] || impl['versions']
|
|
73
|
-
next unless impl_versions
|
|
74
|
-
|
|
75
|
-
impl_versions.each do |version_spec|
|
|
76
|
-
equals = version_spec[:equals] || version_spec['equals']
|
|
77
|
-
file = version_spec[:file] || version_spec['file']
|
|
78
|
-
next unless equals && file
|
|
79
|
-
|
|
80
|
-
# Build full path: tools/tool_name/implementation_name/file.yaml
|
|
81
|
-
full_path = File.join(register_path, 'tools', name.to_s, impl_name.to_s, file)
|
|
82
|
-
versions[full_path] = equals
|
|
83
|
-
end
|
|
84
|
-
end
|
|
113
|
+
register = register_path ? at(register_path) : default
|
|
114
|
+
register.list_versions(name)
|
|
115
|
+
end
|
|
85
116
|
|
|
86
|
-
|
|
117
|
+
# @deprecated Use Register.default.load_tool_yaml instead
|
|
118
|
+
def load_tool_yaml(name, options = {})
|
|
119
|
+
register = options[:register_path] ? at(options[:register_path]) : default
|
|
120
|
+
register.load_tool_yaml(name, version: options[:version])
|
|
87
121
|
end
|
|
88
122
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# @
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
register_path = options[:register_path] || effective_register_path
|
|
123
|
+
# @deprecated Use Register.default.load_implementation_index instead
|
|
124
|
+
def load_implementation_index(tool_name, options = {})
|
|
125
|
+
register = options[:register_path] ? at(options[:register_path]) : default
|
|
126
|
+
register.load_implementation_index(tool_name)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @deprecated Use Register.default.load_implementation_version instead
|
|
130
|
+
def load_implementation_version(tool_name, implementation_name, file_path, options = {})
|
|
131
|
+
register = options[:register_path] ? at(options[:register_path]) : default
|
|
132
|
+
register.load_implementation_version(tool_name, implementation_name, file_path)
|
|
133
|
+
end
|
|
101
134
|
|
|
135
|
+
# @deprecated Use Register.default.load_interface instead
|
|
136
|
+
def load_interface(path, options = {})
|
|
137
|
+
register = options[:register_path] ? at(options[:register_path]) : default
|
|
138
|
+
register.load_interface(path)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @deprecated Use Register.default methods instead
|
|
142
|
+
def load_tool_metadata(name, options = {})
|
|
102
143
|
# First try exact name match
|
|
103
|
-
yaml_content = load_tool_yaml(name, options
|
|
144
|
+
yaml_content = load_tool_yaml(name, options)
|
|
104
145
|
if yaml_content
|
|
105
146
|
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
106
|
-
|
|
147
|
+
if hash
|
|
148
|
+
return ToolMetadata.from_hash(hash, tool_name: name.to_s,
|
|
149
|
+
register_path: options[:register_path] || default.path)
|
|
150
|
+
end
|
|
107
151
|
end
|
|
108
152
|
|
|
109
153
|
# If not found, try interface-based discovery using ToolIndex
|
|
@@ -113,8 +157,7 @@ module Ukiryu
|
|
|
113
157
|
result = index.find_by_interface(name.to_sym)
|
|
114
158
|
return result if result
|
|
115
159
|
|
|
116
|
-
# Try interface name with common version suffix
|
|
117
|
-
# This handles tools where the interface is defined with version suffix
|
|
160
|
+
# Try interface name with common version suffix
|
|
118
161
|
name_str = name.to_s
|
|
119
162
|
[:"#{name_str}/1.0", :"#{name_str}/1", :"v#{name_str}/1.0"].each do |versioned_interface|
|
|
120
163
|
result = index.find_by_interface(versioned_interface)
|
|
@@ -124,57 +167,7 @@ module Ukiryu
|
|
|
124
167
|
nil
|
|
125
168
|
end
|
|
126
169
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
# @param name [String, Symbol] the tool name
|
|
130
|
-
# @param options [Hash] loading options
|
|
131
|
-
# @option options [String] :version specific version to load
|
|
132
|
-
# @option options [String] :register_path path to register
|
|
133
|
-
# @return [String, nil] the YAML content or nil if not found
|
|
134
|
-
def load_tool_yaml(name, options = {})
|
|
135
|
-
register_path = options[:register_path] || effective_register_path
|
|
136
|
-
|
|
137
|
-
return nil unless register_path
|
|
138
|
-
|
|
139
|
-
# Convert to string for path operations
|
|
140
|
-
name_str = name.to_s
|
|
141
|
-
|
|
142
|
-
# Try version-specific directory first
|
|
143
|
-
version = options[:version]
|
|
144
|
-
if version
|
|
145
|
-
file = File.join(register_path, 'tools', name_str, "#{version}.yaml")
|
|
146
|
-
return File.read(file) if File.exist?(file)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Use cached version list if available
|
|
150
|
-
versions = list_versions(name_str, register_path: register_path)
|
|
151
|
-
|
|
152
|
-
if versions.empty?
|
|
153
|
-
# Try the old format (single file per tool)
|
|
154
|
-
file = File.join(register_path, 'tools', "#{name_str}.yaml")
|
|
155
|
-
return File.read(file) if File.exist?(file)
|
|
156
|
-
|
|
157
|
-
return nil
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Return specific version if requested
|
|
161
|
-
if version
|
|
162
|
-
version_file = versions.keys.find { |f| File.basename(f, '.yaml') == version }
|
|
163
|
-
return version_file ? File.read(version_file) : nil
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Return the latest version (already sorted from scan_tool_versions)
|
|
167
|
-
File.read(versions.keys.last)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Validate a tool profile against the schema
|
|
171
|
-
#
|
|
172
|
-
# @param name [String, Symbol] the tool name
|
|
173
|
-
# @param options [Hash] validation options
|
|
174
|
-
# @option options [String] :version specific version to validate
|
|
175
|
-
# @option options [String] :register_path path to register
|
|
176
|
-
# @option options [String] :schema_path path to schema file
|
|
177
|
-
# @return [ValidationResult] the validation result
|
|
170
|
+
# @deprecated Use Register.default.validate methods instead
|
|
178
171
|
def validate_tool(name, options = {})
|
|
179
172
|
yaml_content = load_tool_yaml(name, options)
|
|
180
173
|
return Models::ValidationResult.not_found(name.to_s) unless yaml_content
|
|
@@ -190,145 +183,393 @@ module Ukiryu
|
|
|
190
183
|
end
|
|
191
184
|
end
|
|
192
185
|
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
# @param options [Hash] validation options
|
|
196
|
-
# @option options [String] :register_path path to register
|
|
197
|
-
# @option options [String] :schema_path path to schema file
|
|
198
|
-
# @return [Array<ValidationResult>] list of validation results
|
|
186
|
+
# @deprecated Use Register.default.validate methods instead
|
|
199
187
|
def validate_all_tools(options = {})
|
|
200
188
|
tools.map do |tool_name|
|
|
201
189
|
validate_tool(tool_name, options)
|
|
202
190
|
end
|
|
203
191
|
end
|
|
204
192
|
|
|
205
|
-
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
# Discover and create the default register
|
|
206
196
|
#
|
|
207
|
-
# @
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
197
|
+
# @return [Register] the discovered register
|
|
198
|
+
def discover
|
|
199
|
+
path, source = discover_path
|
|
200
|
+
raise NotFoundError, 'No register found and auto-clone failed' unless path
|
|
201
|
+
|
|
202
|
+
register = new(path, source: source)
|
|
203
|
+
register.ensure_exists!
|
|
204
|
+
register
|
|
205
|
+
end
|
|
214
206
|
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
# Discover the register path without auto-cloning
|
|
208
|
+
#
|
|
209
|
+
# @return [Array<String, Symbol>, nil] path and source, or nil
|
|
210
|
+
def discover_path
|
|
211
|
+
# 1. Check UKIRYU_REGISTER environment variable
|
|
212
|
+
env_path = ENV['UKIRYU_REGISTER']
|
|
213
|
+
return [env_path, :env] if env_path && Dir.exist?(env_path)
|
|
214
|
+
|
|
215
|
+
# 2. Check development register (sibling to gem source)
|
|
216
|
+
dev_path = calculate_dev_path
|
|
217
|
+
return [dev_path.to_s, :dev] if dev_path&.exist?
|
|
218
|
+
|
|
219
|
+
# 3. Use user clone (may need to be created)
|
|
220
|
+
user_path = File.expand_path(DEFAULT_USER_PATH)
|
|
221
|
+
return [user_path, :user] if Dir.exist?(user_path) && valid_structure?(user_path)
|
|
222
|
+
|
|
223
|
+
# 4. Return user path for auto-clone
|
|
224
|
+
[user_path, :user]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Resolve path without triggering auto-clone
|
|
228
|
+
#
|
|
229
|
+
# @return [String, nil] the path or nil
|
|
230
|
+
def resolve_path_without_clone
|
|
231
|
+
env_path = ENV['UKIRYU_REGISTER']
|
|
232
|
+
return env_path if env_path && Dir.exist?(env_path)
|
|
233
|
+
|
|
234
|
+
dev_path = calculate_dev_path
|
|
235
|
+
return dev_path.to_s if dev_path&.exist?
|
|
217
236
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return nil unless hash
|
|
237
|
+
user_path = File.expand_path(DEFAULT_USER_PATH)
|
|
238
|
+
return user_path if Dir.exist?(user_path) && valid_structure?(user_path)
|
|
221
239
|
|
|
222
|
-
|
|
240
|
+
nil
|
|
223
241
|
end
|
|
224
242
|
|
|
225
|
-
#
|
|
243
|
+
# Calculate the development register path
|
|
226
244
|
#
|
|
227
|
-
# @
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
245
|
+
# @return [Pathname, nil] the dev path or nil
|
|
246
|
+
def calculate_dev_path
|
|
247
|
+
this_file = Pathname.new(__FILE__).realpath
|
|
248
|
+
# lib/ukiryu/register.rb -> ../../../register
|
|
249
|
+
this_file.parent.parent.parent.join('register')
|
|
250
|
+
rescue StandardError
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
233
253
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
254
|
+
# Check if a path has valid register structure
|
|
255
|
+
#
|
|
256
|
+
# @param path [String] the path to check
|
|
257
|
+
# @return [Boolean] true if valid
|
|
258
|
+
def valid_structure?(path)
|
|
259
|
+
return false unless path && Dir.exist?(path)
|
|
238
260
|
|
|
239
|
-
|
|
261
|
+
tools_dir = File.join(path, 'tools')
|
|
262
|
+
return false unless Dir.exist?(tools_dir)
|
|
240
263
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
264
|
+
# Must have at least one tool definition
|
|
265
|
+
Dir.glob(File.join(tools_dir, '*', '*.yaml')).any?
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Initialize a new Register
|
|
270
|
+
#
|
|
271
|
+
# @param path [String] the filesystem path
|
|
272
|
+
# @param source [Symbol] how the register was discovered
|
|
273
|
+
def initialize(path, source: :unknown)
|
|
274
|
+
@path = path
|
|
275
|
+
@source = source
|
|
276
|
+
@version_cache = {}
|
|
277
|
+
end
|
|
247
278
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
279
|
+
# Check if the register exists on disk
|
|
280
|
+
#
|
|
281
|
+
# @return [Boolean] true if the register directory exists
|
|
282
|
+
def exists?
|
|
283
|
+
Dir.exist?(path)
|
|
284
|
+
end
|
|
251
285
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
286
|
+
# Check if the register has valid structure
|
|
287
|
+
#
|
|
288
|
+
# @return [Boolean] true if valid
|
|
289
|
+
def valid?
|
|
290
|
+
self.class.send(:valid_structure?, path)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Ensure the register exists, cloning if necessary
|
|
294
|
+
#
|
|
295
|
+
# @raise [CloneError] if cloning fails
|
|
296
|
+
def ensure_exists!
|
|
297
|
+
return if exists? && valid?
|
|
298
|
+
|
|
299
|
+
clone!
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Clone the register from GitHub
|
|
303
|
+
#
|
|
304
|
+
# @raise [CloneError] if cloning fails
|
|
305
|
+
def clone!
|
|
306
|
+
raise CloneError, "Register already exists at #{path}" if exists? && valid?
|
|
307
|
+
|
|
308
|
+
parent_dir = File.dirname(path)
|
|
309
|
+
FileUtils.mkdir_p(parent_dir) unless Dir.exist?(parent_dir)
|
|
310
|
+
|
|
311
|
+
print "Cloning register from #{REMOTE_URL}..." if $stdout.tty?
|
|
312
|
+
|
|
313
|
+
begin
|
|
314
|
+
Git.clone(REMOTE_URL, path, quiet: true)
|
|
315
|
+
rescue Git::Error => e
|
|
316
|
+
FileUtils.rm_rf(path) if Dir.exist?(path)
|
|
317
|
+
raise CloneError, clone_error_message(e)
|
|
255
318
|
end
|
|
256
319
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
# @param file_path [String] the file path relative to implementation directory
|
|
262
|
-
# @param options [Hash] loading options
|
|
263
|
-
# @option options [String] :register_path path to register
|
|
264
|
-
# @return [Models::ImplementationVersion, nil] the implementation version or nil if not found
|
|
265
|
-
def load_implementation_version(tool_name, implementation_name, file_path, options = {})
|
|
266
|
-
register_path = options[:register_path] || effective_register_path
|
|
320
|
+
puts 'done' if $stdout.tty?
|
|
321
|
+
|
|
322
|
+
raise CloneError, 'Register clone validation failed' unless valid?
|
|
323
|
+
end
|
|
267
324
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
325
|
+
# Update the register (git pull)
|
|
326
|
+
#
|
|
327
|
+
# @raise [Error] if update fails
|
|
328
|
+
def update!
|
|
329
|
+
return clone! unless exists?
|
|
330
|
+
|
|
331
|
+
begin
|
|
332
|
+
null_dev = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL' : '/dev/null'
|
|
333
|
+
old_redirect = ENV['GIT_REDIRECT_STDERR']
|
|
334
|
+
ENV['GIT_REDIRECT_STDERR'] = null_dev
|
|
335
|
+
|
|
336
|
+
print 'Updating register...' if $stdout.tty?
|
|
337
|
+
git = Git.open(path)
|
|
338
|
+
git.pull
|
|
339
|
+
puts 'done' if $stdout.tty?
|
|
340
|
+
ensure
|
|
341
|
+
if old_redirect
|
|
342
|
+
ENV['GIT_REDIRECT_STDERR'] = old_redirect
|
|
343
|
+
else
|
|
344
|
+
ENV.delete('GIT_REDIRECT_STDERR')
|
|
271
345
|
end
|
|
346
|
+
end
|
|
347
|
+
rescue Git::Error => e
|
|
348
|
+
raise Error, "Failed to update register: #{e.message}"
|
|
349
|
+
end
|
|
272
350
|
|
|
273
|
-
|
|
351
|
+
# Get list of all tool names in this register
|
|
352
|
+
#
|
|
353
|
+
# @return [Array<String>] sorted list of tool names
|
|
354
|
+
def tool_names
|
|
355
|
+
tools_dir = File.join(path, 'tools')
|
|
356
|
+
return [] unless Dir.exist?(tools_dir)
|
|
274
357
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
358
|
+
Dir.glob(File.join(tools_dir, '*', 'index.yaml')).map do |index_file|
|
|
359
|
+
File.basename(File.dirname(index_file))
|
|
360
|
+
end.sort
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Check if a tool exists in this register
|
|
364
|
+
#
|
|
365
|
+
# @param name [String, Symbol] the tool name
|
|
366
|
+
# @return [Boolean] true if the tool exists
|
|
367
|
+
def tool?(name)
|
|
368
|
+
index_file = File.join(path, 'tools', name.to_s, 'index.yaml')
|
|
369
|
+
File.exist?(index_file)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Load the implementation index for a tool
|
|
373
|
+
#
|
|
374
|
+
# @param tool_name [String, Symbol] the tool name
|
|
375
|
+
# @return [Models::ImplementationIndex, nil] the index or nil
|
|
376
|
+
def load_implementation_index(tool_name)
|
|
377
|
+
file = File.join(path, 'tools', tool_name.to_s, 'index.yaml')
|
|
378
|
+
return nil unless File.exist?(file)
|
|
281
379
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
380
|
+
yaml_content = File.read(file)
|
|
381
|
+
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
382
|
+
return nil unless hash
|
|
383
|
+
|
|
384
|
+
Models::ImplementationIndex.from_hash(symbolize_keys(hash))
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Load a specific implementation version
|
|
388
|
+
#
|
|
389
|
+
# @param tool_name [String, Symbol] the tool name
|
|
390
|
+
# @param impl_name [String, Symbol] the implementation name
|
|
391
|
+
# @param file_path [String] the file path relative to implementation directory
|
|
392
|
+
# @return [Models::ImplementationVersion, nil] the version or nil
|
|
393
|
+
def load_implementation_version(tool_name, impl_name, file_path)
|
|
394
|
+
file = File.join(path, 'tools', tool_name.to_s, impl_name.to_s, file_path)
|
|
395
|
+
return nil unless File.exist?(file)
|
|
396
|
+
|
|
397
|
+
yaml_content = File.read(file)
|
|
398
|
+
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
399
|
+
return nil unless hash
|
|
400
|
+
|
|
401
|
+
Models::ImplementationVersion.from_hash(symbolize_keys(hash))
|
|
402
|
+
end
|
|
285
403
|
|
|
286
|
-
|
|
404
|
+
# Load raw YAML content for a tool
|
|
405
|
+
#
|
|
406
|
+
# @param name [String, Symbol] the tool name
|
|
407
|
+
# @param version [String, nil] specific version (optional)
|
|
408
|
+
# @return [String, nil] the YAML content or nil
|
|
409
|
+
def load_tool_yaml(name, version: nil)
|
|
410
|
+
name_str = name.to_s
|
|
411
|
+
|
|
412
|
+
# Try version-specific file first
|
|
413
|
+
if version
|
|
414
|
+
file = File.join(path, 'tools', name_str, "#{version}.yaml")
|
|
415
|
+
return File.read(file) if File.exist?(file)
|
|
287
416
|
end
|
|
288
417
|
|
|
289
|
-
|
|
418
|
+
# Get versions from index
|
|
419
|
+
versions = list_versions(name_str)
|
|
420
|
+
return load_legacy_tool_yaml(name_str) if versions.empty?
|
|
290
421
|
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
#
|
|
296
|
-
# @return [String, nil] the register path, or nil if unavailable
|
|
297
|
-
def effective_register_path
|
|
298
|
-
# If manually set, use that
|
|
299
|
-
return @default_register_path if @default_register_path
|
|
300
|
-
|
|
301
|
-
# Otherwise, use RegisterAutoManager (auto-clone if needed)
|
|
302
|
-
# Use :: to reference top-level Ukiryu namespace
|
|
303
|
-
auto_path = ::Ukiryu::RegisterAutoManager.register_path
|
|
304
|
-
warn "[UKIRYU DEBUG] Using RegisterAutoManager path: #{auto_path.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
305
|
-
auto_path
|
|
422
|
+
# Return specific version or latest
|
|
423
|
+
if version
|
|
424
|
+
version_file = versions.keys.find { |f| File.basename(f, '.yaml') == version }
|
|
425
|
+
return version_file ? File.read(version_file) : nil
|
|
306
426
|
end
|
|
307
427
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
428
|
+
File.read(versions.keys.last)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Load an interface definition
|
|
432
|
+
#
|
|
433
|
+
# @param interface_path [String] the interface path (e.g., "gzip/1.0")
|
|
434
|
+
# @return [Models::Interface, nil] the interface or nil
|
|
435
|
+
def load_interface(interface_path)
|
|
436
|
+
file = File.join(path, 'interfaces', "#{interface_path}.yaml")
|
|
437
|
+
return nil unless File.exist?(file)
|
|
438
|
+
|
|
439
|
+
yaml_content = File.read(file)
|
|
440
|
+
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
441
|
+
return nil unless hash
|
|
442
|
+
|
|
443
|
+
Models::Interface.from_hash(symbolize_keys(hash))
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# List available versions for a tool
|
|
447
|
+
#
|
|
448
|
+
# @param tool_name [String, Symbol] the tool name
|
|
449
|
+
# @return [Hash] mapping of full file path to version string
|
|
450
|
+
def list_versions(tool_name)
|
|
451
|
+
index = load_implementation_index(tool_name)
|
|
452
|
+
return {} unless index
|
|
453
|
+
|
|
454
|
+
versions = {}
|
|
455
|
+
index.implementations.each do |impl|
|
|
456
|
+
impl_name = impl[:name] || impl['name']
|
|
457
|
+
impl_versions = impl[:versions] || impl['versions']
|
|
458
|
+
next unless impl_versions
|
|
459
|
+
|
|
460
|
+
impl_versions.each do |version_spec|
|
|
461
|
+
equals = version_spec[:equals] || version_spec['equals']
|
|
462
|
+
file = version_spec[:file] || version_spec['file']
|
|
463
|
+
next unless equals && file
|
|
464
|
+
|
|
465
|
+
full_path = File.join(path, 'tools', tool_name.to_s, impl_name.to_s, file)
|
|
466
|
+
versions[full_path] = equals
|
|
467
|
+
end
|
|
314
468
|
end
|
|
315
469
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
470
|
+
versions
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Get information about this register
|
|
474
|
+
#
|
|
475
|
+
# @return [Hash] register information
|
|
476
|
+
def info
|
|
477
|
+
{
|
|
478
|
+
path: path,
|
|
479
|
+
source: source,
|
|
480
|
+
exists: exists?,
|
|
481
|
+
valid: valid?,
|
|
482
|
+
tools_count: tool_names.count,
|
|
483
|
+
git_info: git_info
|
|
484
|
+
}
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
private
|
|
488
|
+
|
|
489
|
+
# Load legacy single-file tool YAML
|
|
490
|
+
#
|
|
491
|
+
# @param name [String] the tool name
|
|
492
|
+
# @return [String, nil] the YAML content or nil
|
|
493
|
+
def load_legacy_tool_yaml(name)
|
|
494
|
+
file = File.join(path, 'tools', "#{name}.yaml")
|
|
495
|
+
File.read(file) if File.exist?(file)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Get git information for this register
|
|
499
|
+
#
|
|
500
|
+
# @return [Hash, nil] git info or nil
|
|
501
|
+
def git_info
|
|
502
|
+
git_dir = File.join(path, '.git')
|
|
503
|
+
return nil unless Dir.exist?(git_dir)
|
|
504
|
+
|
|
505
|
+
null_dev = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL' : '/dev/null'
|
|
506
|
+
old_redirect = ENV['GIT_REDIRECT_STDERR']
|
|
507
|
+
ENV['GIT_REDIRECT_STDERR'] = null_dev
|
|
508
|
+
|
|
509
|
+
git = Git.open(path)
|
|
510
|
+
log = git.log(1).to_a
|
|
511
|
+
|
|
512
|
+
{
|
|
513
|
+
branch: git.current_branch,
|
|
514
|
+
commit: log.first&.sha&.[](0..7),
|
|
515
|
+
last_update: log.first&.date && Time.at(log.first.date.to_i)
|
|
516
|
+
}
|
|
517
|
+
rescue Git::Error, IOError, Errno::ENOENT
|
|
518
|
+
nil
|
|
519
|
+
ensure
|
|
520
|
+
if old_redirect
|
|
521
|
+
ENV['GIT_REDIRECT_STDERR'] = old_redirect
|
|
522
|
+
else
|
|
523
|
+
ENV.delete('GIT_REDIRECT_STDERR')
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Generate a helpful error message for clone failures
|
|
528
|
+
#
|
|
529
|
+
# @param error [Git::Error] the error
|
|
530
|
+
# @return [String] the error message
|
|
531
|
+
def clone_error_message(error)
|
|
532
|
+
msg = error.message.to_s
|
|
533
|
+
|
|
534
|
+
if msg.include?('cannot find') || msg.include?('not found') || msg.include?('path specified')
|
|
535
|
+
<<~ERROR
|
|
536
|
+
Failed to clone register: #{msg}
|
|
537
|
+
|
|
538
|
+
This error usually means git is not in PATH or the target directory is not accessible.
|
|
539
|
+
|
|
540
|
+
To fix this:
|
|
541
|
+
1. Verify git is installed and in PATH: git --version
|
|
542
|
+
2. On Windows, ensure Git for Windows is installed from https://git-scm.com
|
|
543
|
+
3. Or set UKIRYU_REGISTER to use a local register path
|
|
544
|
+
|
|
545
|
+
Example (Windows):
|
|
546
|
+
set UKIRYU_REGISTER=C:\\path\\to\\register
|
|
547
|
+
|
|
548
|
+
Example (Unix):
|
|
549
|
+
export UKIRYU_REGISTER=/path/to/register
|
|
550
|
+
ERROR
|
|
551
|
+
else
|
|
552
|
+
<<~ERROR
|
|
553
|
+
Failed to clone register from #{REMOTE_URL}: #{msg}
|
|
554
|
+
|
|
555
|
+
To fix this:
|
|
556
|
+
1. Check your internet connection
|
|
557
|
+
2. Verify git is installed: git --version
|
|
558
|
+
3. Manually clone: git clone #{REMOTE_URL} #{path}
|
|
559
|
+
4. Or set UKIRYU_REGISTER to use a local register path
|
|
560
|
+
|
|
561
|
+
Example:
|
|
562
|
+
export UKIRYU_REGISTER=/path/to/register
|
|
563
|
+
ERROR
|
|
331
564
|
end
|
|
332
565
|
end
|
|
566
|
+
|
|
567
|
+
# Recursively symbolize hash keys
|
|
568
|
+
#
|
|
569
|
+
# @param hash [Hash] the hash to symbolize
|
|
570
|
+
# @return [Hash] hash with symbolized keys
|
|
571
|
+
def symbolize_keys(hash)
|
|
572
|
+
Utils.symbolize_keys(hash)
|
|
573
|
+
end
|
|
333
574
|
end
|
|
334
575
|
end
|