unipod 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +79 -0
- data/exe/unipod +13 -0
- data/lib/unipod/command/install.rb +384 -0
- data/lib/unipod/command/push.rb +384 -0
- data/lib/unipod/command/server.rb +115 -0
- data/lib/unipod/command.rb +40 -0
- data/lib/unipod/config.rb +393 -0
- data/lib/unipod/server/cache_manager.rb +1106 -0
- data/lib/unipod/server/download_handler.rb +359 -0
- data/lib/unipod/server/index.html +460 -0
- data/lib/unipod/server/package_builder_queue.rb +255 -0
- data/lib/unipod/server/server.rb +393 -0
- data/lib/unipod/server/server_handler.rb +526 -0
- data/lib/unipod/ui.rb +173 -0
- data/lib/unipod/version.rb +5 -0
- data/lib/unipod.rb +33 -0
- metadata +226 -0
@@ -0,0 +1,393 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module UniPod
|
9
|
+
class Config
|
10
|
+
DEFAULT_CONFIG_PATH = File.join(Dir.home, '.unipod', 'config.yml')
|
11
|
+
|
12
|
+
# 定义在类外部,可以被类方法使用
|
13
|
+
def self.detect_default_cache_dir
|
14
|
+
# 统一使用 macOS 风格的缓存目录,避免使用 ~/.unipod/cache
|
15
|
+
if RUBY_PLATFORM =~ /darwin/
|
16
|
+
# Mac OS - 标准缓存目录
|
17
|
+
File.expand_path('~/Library/Caches/UniPod')
|
18
|
+
elsif RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
19
|
+
# Windows
|
20
|
+
win_cache_dir = ENV['LOCALAPPDATA'] ? File.join(ENV['LOCALAPPDATA'], 'UniPod', 'Cache') : File.expand_path('~/AppData/Local/UniPod/Cache')
|
21
|
+
File.expand_path(win_cache_dir)
|
22
|
+
else
|
23
|
+
# Linux/其他平台 - 遵循 XDG 规范
|
24
|
+
xdg_cache_dir = ENV['XDG_CACHE_HOME'] || File.expand_path('~/.cache')
|
25
|
+
File.expand_path(File.join(xdg_cache_dir, 'unipod'))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# 从Git URL中提取仓库名称,此方法需要与CacheManager中的同名方法保持一致
|
30
|
+
def self.extract_repo_name_from_url(url)
|
31
|
+
if url.nil? || url.empty?
|
32
|
+
return 'default_repo'
|
33
|
+
end
|
34
|
+
|
35
|
+
# 移除尾部的.git(如果有)
|
36
|
+
url = url.sub(/\.git\z/, '')
|
37
|
+
|
38
|
+
# 处理不同格式的URL
|
39
|
+
if url.include?('/')
|
40
|
+
# 处理https://格式或git://格式
|
41
|
+
repo_name = url.split('/').last
|
42
|
+
elsif url.include?(':')
|
43
|
+
# 处理git@格式
|
44
|
+
repo_name = url.split(':').last.split('/').last
|
45
|
+
else
|
46
|
+
# 无法解析时使用默认名称
|
47
|
+
repo_name = 'default_repo'
|
48
|
+
end
|
49
|
+
|
50
|
+
# 确保名称不包含特殊字符
|
51
|
+
repo_name.gsub(/[^a-zA-Z0-9_-]/, '_')
|
52
|
+
end
|
53
|
+
|
54
|
+
DEFAULT_CONFIG = {
|
55
|
+
server: {
|
56
|
+
host: 'localhost',
|
57
|
+
port: 7748,
|
58
|
+
timeout: 120, # 增加默认超时时间
|
59
|
+
max_clients: 10, # 降低最大客户端数量
|
60
|
+
log_level: 'info',
|
61
|
+
session_id: SecureRandom.hex(8) # 为每次启动生成唯一会话ID
|
62
|
+
},
|
63
|
+
cache: {
|
64
|
+
dir: detect_default_cache_dir,
|
65
|
+
max_size: 1024, # MB
|
66
|
+
expiry: 7 # days
|
67
|
+
},
|
68
|
+
repositories: {
|
69
|
+
index_repos: [
|
70
|
+
{
|
71
|
+
name: extract_repo_name_from_url('https://gitee.com/goodunitylib/JoyUnityLibIndex.git'),
|
72
|
+
url: 'https://gitee.com/goodunitylib/JoyUnityLibIndex.git',
|
73
|
+
enabled: true
|
74
|
+
}
|
75
|
+
],
|
76
|
+
custom_repos: [],
|
77
|
+
scope_mappings: {}
|
78
|
+
},
|
79
|
+
unity: {
|
80
|
+
package_format: 'upm', # 'upm' 或 'tgz'
|
81
|
+
preload_packages: true, # 预加载包信息
|
82
|
+
compatibility: '2019.4' # 最低兼容版本
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
class << self
|
87
|
+
def load_config
|
88
|
+
# 初始化默认配置
|
89
|
+
@config = DEFAULT_CONFIG.dup
|
90
|
+
|
91
|
+
# 如果配置文件存在,尝试读取并合并
|
92
|
+
if File.exist?(config_path)
|
93
|
+
begin
|
94
|
+
user_config = YAML.load_file(config_path)
|
95
|
+
# 使用深度合并来确保用户配置覆盖默认配置,但保留未指定的默认值
|
96
|
+
deep_merge!(@config, user_config)
|
97
|
+
rescue => e
|
98
|
+
# 如果配置文件读取失败,使用默认配置,不终止程序
|
99
|
+
puts "注意: 配置文件读取失败,使用默认配置: #{e.message}" if ENV['UNIPOD_VERBOSE'] == 'true'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# 检查环境变量中的配置覆盖
|
104
|
+
apply_env_overrides
|
105
|
+
|
106
|
+
@config
|
107
|
+
end
|
108
|
+
|
109
|
+
# 从环境变量中读取配置
|
110
|
+
def apply_env_overrides
|
111
|
+
# 服务器配置
|
112
|
+
@config[:server][:host] = ENV['UNIPOD_SERVER_HOST'] if ENV['UNIPOD_SERVER_HOST']
|
113
|
+
@config[:server][:port] = ENV['UNIPOD_SERVER_PORT'].to_i if ENV['UNIPOD_SERVER_PORT']
|
114
|
+
|
115
|
+
# 缓存配置
|
116
|
+
@config[:cache][:dir] = ENV['UNIPOD_CACHE_DIR'] if ENV['UNIPOD_CACHE_DIR']
|
117
|
+
|
118
|
+
# 其他环境变量...
|
119
|
+
end
|
120
|
+
|
121
|
+
def save_config
|
122
|
+
# 确保配置目录存在
|
123
|
+
FileUtils.mkdir_p(File.dirname(config_path))
|
124
|
+
|
125
|
+
# 保存配置到文件
|
126
|
+
File.open(config_path, 'w') do |f|
|
127
|
+
f.write(YAML.dump(@config))
|
128
|
+
end
|
129
|
+
|
130
|
+
puts "配置已保存到: #{config_path}" if ENV['UNIPOD_VERBOSE'] == 'true'
|
131
|
+
end
|
132
|
+
|
133
|
+
def config_path
|
134
|
+
@config_path ||= ENV['UNIPOD_CONFIG'] || DEFAULT_CONFIG_PATH
|
135
|
+
end
|
136
|
+
|
137
|
+
def config
|
138
|
+
@config ||= load_config
|
139
|
+
end
|
140
|
+
|
141
|
+
def get(key, default = nil)
|
142
|
+
keys = key.to_s.split('.')
|
143
|
+
value = config
|
144
|
+
|
145
|
+
keys.each do |k|
|
146
|
+
if value.is_a?(Hash) && value.key?(k.to_sym)
|
147
|
+
value = value[k.to_sym]
|
148
|
+
elsif value.is_a?(Hash) && value.key?(k)
|
149
|
+
value = value[k]
|
150
|
+
else
|
151
|
+
return default
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
value
|
156
|
+
end
|
157
|
+
|
158
|
+
def set(key, value)
|
159
|
+
keys = key.to_s.split('.')
|
160
|
+
last_key = keys.pop
|
161
|
+
|
162
|
+
# 导航到最后一个键的父节点
|
163
|
+
node = config
|
164
|
+
keys.each do |k|
|
165
|
+
sym_key = k.to_sym
|
166
|
+
str_key = k.to_s
|
167
|
+
|
168
|
+
key_to_use = node.key?(sym_key) ? sym_key : (node.key?(str_key) ? str_key : sym_key)
|
169
|
+
|
170
|
+
node[key_to_use] ||= {}
|
171
|
+
node = node[key_to_use]
|
172
|
+
end
|
173
|
+
|
174
|
+
# 设置值
|
175
|
+
last_sym_key = last_key.to_sym
|
176
|
+
last_str_key = last_key.to_s
|
177
|
+
|
178
|
+
key_to_use = node.key?(last_sym_key) ? last_sym_key : (node.key?(last_str_key) ? last_str_key : last_sym_key)
|
179
|
+
|
180
|
+
node[key_to_use] = value
|
181
|
+
|
182
|
+
# 保存配置 - 仅当用户显式调用save_config时才写入文件
|
183
|
+
|
184
|
+
value
|
185
|
+
end
|
186
|
+
|
187
|
+
def server_host
|
188
|
+
get('server.host', 'localhost')
|
189
|
+
end
|
190
|
+
|
191
|
+
def server_port
|
192
|
+
get('server.port', 7748)
|
193
|
+
end
|
194
|
+
|
195
|
+
def server_timeout
|
196
|
+
get('server.timeout', 120)
|
197
|
+
end
|
198
|
+
|
199
|
+
def server_max_clients
|
200
|
+
get('server.max_clients', 10)
|
201
|
+
end
|
202
|
+
|
203
|
+
def server_log_level
|
204
|
+
get('server.log_level', 'info')
|
205
|
+
end
|
206
|
+
|
207
|
+
def server_session_id
|
208
|
+
get('server.session_id', SecureRandom.hex(8))
|
209
|
+
end
|
210
|
+
|
211
|
+
def cache_dir
|
212
|
+
# 确保使用默认缓存目录,而不是 ~/.unipod/cache
|
213
|
+
get('cache.dir', detect_default_cache_dir)
|
214
|
+
end
|
215
|
+
|
216
|
+
def cache_max_size
|
217
|
+
get('cache.max_size', 1024) # MB
|
218
|
+
end
|
219
|
+
|
220
|
+
def cache_expiry
|
221
|
+
get('cache.expiry', 7) # days
|
222
|
+
end
|
223
|
+
|
224
|
+
def index_repos
|
225
|
+
config = load_config
|
226
|
+
repos = config['index_repos'] || []
|
227
|
+
|
228
|
+
# 查找Unity项目的manifest.json文件,加载其中的package_index_url
|
229
|
+
manifest_repos = load_unity_manifest_repos
|
230
|
+
|
231
|
+
# 合并从manifest中读取的仓库信息
|
232
|
+
manifest_repos.each do |manifest_repo|
|
233
|
+
# 检查是否已存在同名仓库
|
234
|
+
existing_repo = repos.find { |r| r[:name] == manifest_repo[:name] }
|
235
|
+
if existing_repo
|
236
|
+
# 更新现有仓库信息
|
237
|
+
existing_repo[:package_index_url] = manifest_repo[:package_index_url] if manifest_repo[:package_index_url]
|
238
|
+
else
|
239
|
+
# 添加新仓库
|
240
|
+
repos << manifest_repo
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
repos
|
245
|
+
end
|
246
|
+
|
247
|
+
def custom_repos
|
248
|
+
get('repositories.custom_repos', [])
|
249
|
+
end
|
250
|
+
|
251
|
+
def unity_package_format
|
252
|
+
get('unity.package_format', 'upm')
|
253
|
+
end
|
254
|
+
|
255
|
+
def unity_preload_packages
|
256
|
+
get('unity.preload_packages', true)
|
257
|
+
end
|
258
|
+
|
259
|
+
def unity_compatibility
|
260
|
+
get('unity.compatibility', '2019.4')
|
261
|
+
end
|
262
|
+
|
263
|
+
def add_index_repo(name, url, enabled = true)
|
264
|
+
repos = get('repositories.index_repos', [])
|
265
|
+
|
266
|
+
# 检查是否已存在同名仓库
|
267
|
+
existing_repo = repos.find { |r| r[:name] == name }
|
268
|
+
if existing_repo
|
269
|
+
# 更新现有仓库信息
|
270
|
+
existing_repo[:url] = url
|
271
|
+
existing_repo[:enabled] = enabled
|
272
|
+
else
|
273
|
+
# 添加新仓库
|
274
|
+
repos << {
|
275
|
+
name: name,
|
276
|
+
url: url,
|
277
|
+
enabled: enabled
|
278
|
+
}
|
279
|
+
end
|
280
|
+
|
281
|
+
set('repositories.index_repos', repos)
|
282
|
+
end
|
283
|
+
|
284
|
+
def add_scope_mapping(scope, repo_name)
|
285
|
+
mappings = get('repositories.scope_mappings', {})
|
286
|
+
mappings[scope] = repo_name
|
287
|
+
set('repositories.scope_mappings', mappings)
|
288
|
+
end
|
289
|
+
|
290
|
+
def get_repo_for_scope(scope)
|
291
|
+
mappings = get('repositories.scope_mappings', {})
|
292
|
+
mappings[scope]
|
293
|
+
end
|
294
|
+
|
295
|
+
def remove_index_repo(name)
|
296
|
+
repos = index_repos.dup
|
297
|
+
repos.reject! { |r| r[:name] == name }
|
298
|
+
set('repositories.index_repos', repos)
|
299
|
+
end
|
300
|
+
|
301
|
+
def add_custom_repo(name, url, type = 'git', enabled = true)
|
302
|
+
repos = custom_repos.dup
|
303
|
+
|
304
|
+
# 检查是否已存在
|
305
|
+
existing = repos.find { |r| r[:name] == name }
|
306
|
+
if existing
|
307
|
+
existing[:url] = url
|
308
|
+
existing[:type] = type
|
309
|
+
existing[:enabled] = enabled
|
310
|
+
else
|
311
|
+
repos << { name: name, url: url, type: type, enabled: enabled }
|
312
|
+
end
|
313
|
+
|
314
|
+
set('repositories.custom_repos', repos)
|
315
|
+
end
|
316
|
+
|
317
|
+
# 从Unity的manifest.json文件加载仓库信息
|
318
|
+
def load_unity_manifest_repos
|
319
|
+
repos = []
|
320
|
+
|
321
|
+
# 查找当前目录及其父目录中的manifest.json文件
|
322
|
+
manifest_path = find_unity_manifest
|
323
|
+
return repos unless manifest_path
|
324
|
+
|
325
|
+
begin
|
326
|
+
manifest = JSON.parse(File.read(manifest_path))
|
327
|
+
|
328
|
+
# 检查是否有scopedRegistries配置
|
329
|
+
if manifest['scopedRegistries'] && manifest['scopedRegistries'].is_a?(Array)
|
330
|
+
manifest['scopedRegistries'].each do |registry|
|
331
|
+
next unless registry['name'] && registry['url']
|
332
|
+
|
333
|
+
repo = {
|
334
|
+
name: registry['name'].downcase.gsub(/[^a-z0-9_-]/, '_'),
|
335
|
+
url: registry['url'],
|
336
|
+
package_index_url: registry['package_index_url'] || registry['index_url'] # 支持两种字段名称
|
337
|
+
}
|
338
|
+
|
339
|
+
# 如果有package_index_url或index_url,才添加仓库
|
340
|
+
if repo[:package_index_url]
|
341
|
+
repos << repo
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
rescue => e
|
346
|
+
# 忽略解析错误
|
347
|
+
puts "无法解析Unity的manifest.json文件: #{e.message}" if ENV['UNIPOD_VERBOSE'] == 'true'
|
348
|
+
end
|
349
|
+
|
350
|
+
repos
|
351
|
+
end
|
352
|
+
|
353
|
+
# 在当前目录及其父目录中查找Unity的manifest.json文件
|
354
|
+
def find_unity_manifest
|
355
|
+
current_dir = Dir.pwd
|
356
|
+
max_depth = 10 # 限制向上查找的层数
|
357
|
+
|
358
|
+
depth = 0
|
359
|
+
while depth < max_depth
|
360
|
+
# 检查当前目录是否有manifest.json文件
|
361
|
+
manifest_path = File.join(current_dir, 'Packages', 'manifest.json')
|
362
|
+
return manifest_path if File.exist?(manifest_path)
|
363
|
+
|
364
|
+
# 检查是否已到达根目录
|
365
|
+
parent_dir = File.dirname(current_dir)
|
366
|
+
break if parent_dir == current_dir # 已到达根目录
|
367
|
+
|
368
|
+
current_dir = parent_dir
|
369
|
+
depth += 1
|
370
|
+
end
|
371
|
+
|
372
|
+
nil # 未找到manifest.json文件
|
373
|
+
end
|
374
|
+
|
375
|
+
private
|
376
|
+
|
377
|
+
# 深度合并哈希
|
378
|
+
def deep_merge!(target, source)
|
379
|
+
source.each do |key, value|
|
380
|
+
target_value = target[key]
|
381
|
+
|
382
|
+
if target_value.is_a?(Hash) && value.is_a?(Hash)
|
383
|
+
deep_merge!(target_value, value)
|
384
|
+
else
|
385
|
+
target[key] = value
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
target
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|