cocoapods-yaml 1.0.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.
@@ -0,0 +1,774 @@
1
+ require_relative 'yaml_pod'
2
+ require_relative '../../version'
3
+ require_relative '../my_constants'
4
+ require_relative '../check/xcodeproj'
5
+ require_relative '../variable'
6
+ require 'colored2'
7
+ require 'yaml'
8
+ require 'json'
9
+ require 'shellwords'
10
+
11
+ module Pod
12
+ class YamlDependencies
13
+
14
+ # 需要集成依赖的主工程target列表(application/app扩展)
15
+ # @return [Array]
16
+ attr_accessor :targets
17
+
18
+ # 所有yaml文件定义的依赖
19
+ # @return {String => {String => Pod:YamlPod}} key: yaml文件名
20
+ attr_accessor :yaml_deps
21
+
22
+ # 所有target最终生效的依赖(已合并LocalDependencies覆盖)
23
+ # @return {target对象 => [Pod:YamlPod]}
24
+ attr_accessor :targets_dependencies
25
+
26
+ # 每个target依赖文件声明的SOURCES(有序)
27
+ # @return {String => [String]} key: yaml文件名
28
+ attr_accessor :file_sources
29
+
30
+ attr_accessor :local_yaml_name
31
+
32
+ def initialize
33
+ super
34
+ @targets = []
35
+ @yaml_deps = {}
36
+ @targets_dependencies = {}
37
+ @file_sources = {}
38
+ @local_yaml_name = MyConstants::LOCAL_DEPENDENCIES_YAML
39
+ end
40
+
41
+ # 某个target对应的依赖yaml文件名
42
+ def target_yaml_name(target_name)
43
+ "#{target_name}#{MyConstants::DEPENDENCIES_YAML_SUFFIX}"
44
+ end
45
+
46
+ # 某个target对应的包内依赖清单文件名(仅Debug入包)
47
+ def target_json_name(target_name)
48
+ "#{target_name}#{MyConstants::DEPENDENCIES_JSON_SUFFIX}"
49
+ end
50
+
51
+ # 仅主App(.application)和App扩展(.app-extension)需要通过yaml集成依赖
52
+ def integratable_target?(target)
53
+ type = target.product_type.to_s
54
+ type.end_with?('.application') || type.end_with?('.app-extension')
55
+ end
56
+
57
+ def install_yaml_dep(podfile)
58
+ analysis_yaml_dep
59
+
60
+ # 为路径依赖的组件仓库开启 SNAPSHOT tag 自动同步(最小权限: 只配置被本地引用的仓库)
61
+ ensure_local_path_tag_sync
62
+
63
+ # 将YAML中声明的私有源注册为Podfile顶层source,屏蔽CocoaPods隐式追加的CDN trunk源
64
+ configure_private_sources(podfile)
65
+
66
+ install_dependencies(targets_dependencies, podfile) do|name|
67
+ yield(name) if block_given?
68
+ end
69
+ end
70
+
71
+ # 是否允许使用CocoaPods官方CDN trunk源。
72
+ # 由LocalDependencies.yaml顶层 ENABLE_TRUNK 控制(工程级,不提交git,只影响本工程),默认false
73
+ def trunk_enabled?
74
+ content = read_yaml_content(local_yaml_name)
75
+ content ? content['ENABLE_TRUNK'] == true : false
76
+ end
77
+
78
+ # Podfile未声明任何顶层source时,CocoaPods会隐式加入CDN trunk源(cdn.cocoapods.org)。
79
+ # 团队组件(含三方库fork)全部在私有源,无需trunk:trunk既会因网络问题导致pod install失败,
80
+ # 也会与私有源存在同名podspec(如WechatOpenSDK)而产生"Found multiple specifications"警告。
81
+ # 这里按各target依赖文件中SOURCES的声明顺序注册为顶层source(把二进制源放在第一位,
82
+ # 可保证间接依赖优先命中二进制产物),逐依赖显式指定的source追加在后。
83
+ def configure_private_sources(podfile)
84
+ if trunk_enabled?
85
+ log.detail("已开启CDN trunk源(LocalDependencies.yaml中ENABLE_TRUNK: true)")
86
+ return
87
+ end
88
+
89
+ private_sources = []
90
+ targets.each do |target|
91
+ name = target_yaml_name(target.name)
92
+ Array(file_sources[name]).each { |repo| private_sources << repo }
93
+ dependencies = yaml_deps[name]
94
+ next unless dependencies.is_a?(Hash)
95
+ dependencies.each_value do |dependency|
96
+ repo = dependency.source.to_s.strip
97
+ private_sources << repo unless repo.empty?
98
+ end
99
+ end
100
+ private_sources.uniq!
101
+
102
+ log.info('')
103
+ if private_sources.empty?
104
+ log.warn("target依赖文件未声明任何私有源(SOURCES),保留CocoaPods默认CDN trunk源")
105
+ return
106
+ end
107
+
108
+ private_sources.each { |repo| podfile.source(repo) }
109
+ log.detail("已限定使用#{private_sources.size}个私有specs源,不使用CDN trunk")
110
+ private_sources.each { |repo| log.info(" - #{repo}") }
111
+ log.info(" 如需恢复trunk,在#{local_yaml_name}顶层设置 ENABLE_TRUNK: true")
112
+ log.info('')
113
+ end
114
+
115
+ # SNAPSHOT 同名 tag 每次发布会被删除后前移,而 git 默认 fetch 不会覆盖本地已存在的 tag,
116
+ # 导致本地/同事本地的组件仓 tag 停留在旧位置。
117
+ # 这里仅对 LocalDependencies.yaml 中以 path 方式引用、且本身是 git 仓库的目录,
118
+ # 幂等写入仓库级(.git/config)配置:
119
+ # 1. fetch refspec 对 tags 加 '+' 前缀,允许强制更新已移动的 tag
120
+ # 2. tagOpt=--no-tags 关闭默认的 tag 自动跟随,避免与上面的显式 refspec 冲突
121
+ # (否则会报 multiple updates for ref 'refs/tags/xxx' not allowed)
122
+ def ensure_local_path_tag_sync
123
+ local_deps = yaml_deps[local_yaml_name]
124
+ return unless local_deps.is_a?(Hash)
125
+
126
+ tag_refspec = '+refs/tags/*:refs/tags/*'
127
+ configured = 0
128
+
129
+ local_deps.values.map(&:path).compact.uniq.each do |raw_path|
130
+ repo_path = File.expand_path(raw_path)
131
+ next unless File.exist?(File.join(repo_path, '.git'))
132
+
133
+ begin
134
+ existing = `git -C #{Shellwords.escape(repo_path)} config --get-all remote.origin.fetch 2>/dev/null`.to_s
135
+ next if existing.lines.map(&:strip).include?(tag_refspec)
136
+
137
+ # 数组形式system避免路径/refs注入,并检查退出码,失败不静默
138
+ add_ok = system('git', '-C', repo_path, 'config', '--add', 'remote.origin.fetch', tag_refspec,
139
+ out: File::NULL, err: File::NULL)
140
+ tag_ok = add_ok && system('git', '-C', repo_path, 'config', 'remote.origin.tagOpt', '--no-tags',
141
+ out: File::NULL, err: File::NULL)
142
+ unless add_ok && tag_ok
143
+ log.warn("路径仓库SNAPSHOT tag自动同步配置未生效(git config执行失败): #{repo_path}")
144
+ next
145
+ end
146
+
147
+ log.info('') if configured == 0
148
+ log.detail("已开启路径仓库的SNAPSHOT tag自动同步(后续 git fetch/pull 会自动前移): #{repo_path}")
149
+ configured += 1
150
+ rescue => e
151
+ log.warn("配置路径仓库tag同步失败(#{repo_path}): #{e.message}")
152
+ end
153
+ end
154
+
155
+ log.detail("共为#{configured}个路径仓库开启了SNAPSHOT tag自动同步") if configured > 0
156
+ end
157
+
158
+ def install_dependencies(targets_dependencies, podfile)
159
+ # 存储所有的依赖
160
+ targets_dependencies.each { |target_obj, dependencies|
161
+ # 零依赖target(如尚未声明依赖的扩展)不生成空的Podfile target块
162
+ next if dependencies.empty?
163
+ target_name = target_obj.name
164
+ podfile.target target_name do
165
+ dependencies.each { |dependency|
166
+ if dependency.version
167
+ podfile.pod dependency.pod, dependency.version, dependency.option
168
+ else
169
+ podfile.pod dependency.pod, dependency.option
170
+ end
171
+ }
172
+ yield(target_name) if block_given?
173
+ end
174
+ }
175
+ end
176
+
177
+ # 解析所有target在yaml文件中定义的依赖
178
+ # @return targets_dependencies
179
+ def analysis_yaml_dep
180
+ init_main_targets
181
+ check_orphan_yaml_files
182
+ generate_yaml
183
+ read_yaml_deps
184
+ resolve_targets_deps
185
+ print_targets_deps
186
+ targets_dependencies
187
+ end
188
+
189
+ # 构建各target最终依赖的结构化记录(供包内依赖清单json使用,不生成模板文件、不打印日志)
190
+ # @return {String => Array<Hash>} key: target名
191
+ def build_dependency_records
192
+ init_main_targets
193
+ read_yaml_deps
194
+ resolve_targets_deps
195
+ records = {}
196
+ targets_dependencies.each do |target, dependencies|
197
+ records[target.name] = dependencies.map { |dependency| dependency_record(dependency) }
198
+ end
199
+ records
200
+ end
201
+
202
+ # 归一化依赖记录(空值与默认值不输出,保持与yaml精简风格一致)
203
+ def dependency_record(dependency)
204
+ record = { 'pod' => dependency.pod }
205
+ %w[version git tag branch podspec path].each do |field|
206
+ value = dependency.public_send(field)
207
+ record[field] = value unless value.nil? || value.to_s.strip.empty?
208
+ end
209
+ record['configurations'] = dependency.configurations if Array(dependency.configurations).any?
210
+ record['source'] = dependency.source unless dependency.source.to_s.strip.empty?
211
+ # 默认true,仅false(本地覆盖默认值)时显式输出
212
+ record['inhibit_warnings'] = false if dependency.inhibit_warnings == false
213
+ record['local'] = true if dependency.from_local
214
+ record
215
+ end
216
+
217
+ # 合并LocalDependencies.yaml的本地声明到某个target的依赖上
218
+ # 规则:
219
+ # 1. 对target文件中已声明的同名组件(按组件根名匹配)做本地覆盖;
220
+ # 覆盖时清除该组件在target中的全部旧声明(含各subspec),
221
+ # 否则root(path来源)与subspec(版本号来源)会触发CocoaPods多源冲突
222
+ # 2. target文件未声明的"全新组件"不在此处处理,
223
+ # 由inject_local_only_dependencies统一注入主App target
224
+ # @param file_deps {String => YamlPod} target依赖文件中的依赖
225
+ # @return {String => YamlPod}
226
+ def merge_local_dependencies(file_deps)
227
+ result = file_deps.dup
228
+ local_deps = yaml_deps[local_yaml_name]
229
+ return result unless local_deps.is_a?(Hash)
230
+
231
+ declared_roots = result.keys.map { |key| key.split('/').first }.uniq
232
+
233
+ local_deps.each do |name, dep|
234
+ real_name = name.split('/')[0]
235
+ next unless declared_roots.include?(real_name)
236
+
237
+ result.delete_if { |key, _| key.split('/').first == real_name }
238
+ # 覆盖明细不单独打印,下方依赖逐行输出中的[本地覆盖]标记已体现
239
+ result[name] = dep
240
+ end
241
+ result
242
+ end
243
+
244
+ def resolve_targets_deps
245
+ merged_hashes = {}
246
+ targets.each do |target|
247
+ file_deps = yaml_deps[target_yaml_name(target.name)] || {}
248
+ merged_hashes[target] = merge_local_dependencies(file_deps)
249
+ end
250
+
251
+ # LocalDependencies中声明、但所有target依赖文件都未声明的全新组件,注入主App target
252
+ inject_local_only_dependencies(merged_hashes)
253
+
254
+ merged_hashes.each { |target, deps| targets_dependencies[target] = deps.values }
255
+ targets_dependencies
256
+ end
257
+
258
+ # LocalDependencies.yaml里声明、但没有任何target依赖文件声明的组件(本地联调主动引入的新组件)。
259
+ # 统一注入主App target(.application):
260
+ # - 本地开发新增组件的标准路径就是写LocalDependencies,理应能直接pod install集成
261
+ # - 裁剪(TRIM_UNUSED_POD)对LocalDependencies中的组件本就整体保留,注入后同样不会被剪掉
262
+ # 不向App扩展target注入:扩展通常只允许子集依赖,自动塞入无关库会拖大扩展体积甚至引发链接问题,
263
+ # 扩展需要新组件时请在对应<Target>Dependencies.yaml显式声明
264
+ # @param merged_hashes {target => {String => YamlPod}} 各target已合并本地覆盖的依赖
265
+ def inject_local_only_dependencies(merged_hashes)
266
+ local_deps = yaml_deps[local_yaml_name]
267
+ return unless local_deps.is_a?(Hash) && local_deps.any?
268
+
269
+ declared_roots = targets.flat_map { |target| (yaml_deps[target_yaml_name(target.name)] || {}).keys }
270
+ .map { |key| key.split('/').first }
271
+ .uniq
272
+
273
+ new_entries = local_deps.reject { |name, _| declared_roots.include?(name.split('/').first) }
274
+ return if new_entries.empty?
275
+
276
+ main_target = targets.find { |target| target.product_type.to_s.end_with?('.application') }
277
+ new_roots = new_entries.keys.map { |name| name.split('/').first }.uniq
278
+ unless main_target
279
+ log.warn("LocalDependencies.yaml中的新组件(#{new_roots.join('、')})未在任何target依赖文件中声明,且未找到主App target,无法自动集成;请在对应<Target>Dependencies.yaml中声明")
280
+ return
281
+ end
282
+
283
+ # version引用且未显式指定source时,借用主App target依赖文件SOURCES的第一个源,
284
+ # 与target依赖文件"不写source默认取SOURCES第一个"的语义保持一致;
285
+ # path/podspec/git引用不经过specs源解析,无需补source
286
+ default_source = file_sources[target_yaml_name(main_target.name)]&.first
287
+
288
+ merged = merged_hashes[main_target]
289
+ added_roots = []
290
+ new_entries.each do |name, dep|
291
+ next if merged.key?(name)
292
+
293
+ dep.local_injected = true
294
+ dep.source = default_source if dep.version && default_source && dep.source.to_s.strip.empty?
295
+ merged[name] = dep
296
+ added_roots << name.split('/').first
297
+ end
298
+
299
+ log.detail("LocalDependencies中的新组件(#{added_roots.uniq.join('、')})未在target依赖文件声明,已自动加入主App target[#{main_target.name}](不会被裁剪;App扩展target不会自动注入,需在对应依赖文件显式声明)") if added_roots.any?
300
+ end
301
+
302
+ # 读取所有target和本地覆盖的yaml文件
303
+ def read_yaml_deps
304
+ targets.each { |target| read_yaml_deps_file(target_yaml_name(target.name)) }
305
+ read_yaml_deps_file(local_yaml_name)
306
+ end
307
+
308
+ def read_yaml_deps_file(yaml_name)
309
+
310
+ yaml_content = read_yaml_content(yaml_name)
311
+
312
+ unless yaml_content
313
+ return
314
+ end
315
+
316
+ unless yaml_content["PODS"]
317
+ log.detail("#{yaml_name}共0个依赖(文件内的key[PODS]不存在)")
318
+ return
319
+ end
320
+
321
+ if yaml_content["PODS"].class != Array
322
+ log.detail("#{yaml_name}共0个依赖(文件配置的依赖PODS项不是数组或数组为空)")
323
+ return
324
+ end
325
+
326
+ if yaml_name != local_yaml_name
327
+ sources = yaml_content["SOURCES"]
328
+ # 零依赖文件(如尚未配置的扩展)允许SOURCES留空;一旦声明依赖,SOURCES必须是非空字符串数组
329
+ unless yaml_content["PODS"].empty?
330
+ if sources.class != Array or sources.empty? or sources.any? { |s| s.to_s.strip.empty? }
331
+ log.error("#{yaml_name}的SOURCES字段必须是非空数组,例如: SOURCES: ['git@xxx_binary.git', 'git@xxx_source.git']")
332
+ exit!
333
+ end
334
+ end
335
+ if sources.is_a?(Array) and sources.all? { |s| s.to_s.strip != "" }
336
+ file_sources[yaml_name] = sources.map(&:to_s).map(&:strip)
337
+ end
338
+ end
339
+
340
+ # 依赖未显式指定source时,默认使用SOURCES中的第一个源(通常把二进制源放在第一位)
341
+ default_source = file_sources[yaml_name]&.first
342
+
343
+ # 依赖列表
344
+ dependencies_pods = yaml_content['PODS']
345
+
346
+ # 当前yaml文件的组件名字 用来校验本文件内是否重复
347
+ dependencies_name = []
348
+
349
+ yaml_dependencies = {}
350
+
351
+ # 遍历依赖库列表
352
+ dependencies_pods.each { |dependency|
353
+
354
+ check_pod(dependency, yaml_name, dependencies_name)
355
+
356
+ # source: 显式指定优先,否则取默认源;LocalDependencies无SOURCES时允许为空(path/podspec引用不需要)
357
+ source = dependency['source'].to_s.strip
358
+ source = default_source if source.empty? && default_source
359
+
360
+ # inhibit_warnings: target依赖文件默认true(远程库警告不好修改),本地覆盖文件默认false(开发时能看到本地库警告)
361
+ inhibit_warnings = if dependency.key?('inhibit_warnings')
362
+ !!dependency['inhibit_warnings']
363
+ else
364
+ yaml_name != local_yaml_name
365
+ end
366
+
367
+ # configurations和subspecs cocoapods不能同时支持,所以要展开
368
+ if Array(dependency['subspecs']).size > 0 and Array(dependency['configurations']).size > 0
369
+ dependency['subspecs'].each {|subspec|
370
+ dep = new_yaml_dep(dependency, "#{dependency['pod']}/#{subspec}", source, inhibit_warnings, yaml_name)
371
+ dep.subspecs = nil
372
+ yaml_dependencies[dep.pod] = dep
373
+ }
374
+
375
+ else
376
+ dep = new_yaml_dep(dependency, dependency['pod'], source, inhibit_warnings, yaml_name)
377
+ yaml_dependencies[dep.pod] = dep
378
+ end
379
+ }
380
+
381
+ yaml_deps[yaml_name] = yaml_dependencies
382
+
383
+ yaml_dependencies
384
+ end
385
+
386
+ def new_yaml_dep(dependency, pod_name, source, inhibit_warnings, yaml_name)
387
+ dep = YamlPod.new(pod_name)
388
+ dep.subspecs = dependency['subspecs']
389
+ dep.path = dependency['path']
390
+ dep.podspec = dependency['podspec']
391
+ dep.version = dependency['version']
392
+ dep.git = dependency['git']
393
+ dep.tag = dependency['tag']
394
+ dep.branch = dependency['branch']
395
+ dep.configurations = dependency['configurations']
396
+ dep.inhibit_warnings = inhibit_warnings
397
+ dep.source = source
398
+ dep.from_local = (yaml_name == local_yaml_name)
399
+ dep
400
+ end
401
+
402
+ def check_pod(dependency, yaml_name, dependencies_name)
403
+ # 依赖条目必须是keyValue形式;写成"- Foo"这种纯字符串时后续each_key会抛原始堆栈,这里前置拦截
404
+ unless dependency.is_a?(Hash)
405
+ log.error("#{yaml_name}: 依赖条目必须为keyValue形式(每项以 - pod: 开头),当前条目: #{dependency.inspect}")
406
+ exit!
407
+ end
408
+
409
+ # 不支持提醒
410
+ available_keys = %w[pod subspecs path podspec version git tag branch configurations inhibit_warnings source]
411
+ dependency.each_key { |key|
412
+ unless available_keys.include?(key)
413
+ gem_name = "cocoapods-yaml"
414
+ log.error("#{yaml_name}: 当前#{gem_name}版本(#{CocoaPods::Yaml::VERSION})不支持字段#{key}")
415
+ log.info("请检查对应字段或尝试执行如下命令升级")
416
+ log.info("gem uninstall #{gem_name} && gem install #{gem_name}")
417
+ exit!
418
+ end
419
+ }
420
+
421
+ # 校验字段
422
+ unless dependency['pod']
423
+ log.error("#{yaml_name}: pod 字段(组件名字)必须存在,请检查:")
424
+ exit!
425
+ end
426
+
427
+ current_type = nil
428
+ current_type = check_only_one_dep(dependency, "path", current_type, yaml_name)
429
+ current_type = check_only_one_dep(dependency, "podspec", current_type, yaml_name)
430
+ current_type = check_only_one_dep(dependency, "version", current_type, yaml_name)
431
+ current_type = check_only_one_dep(dependency, "tag", current_type, yaml_name)
432
+ current_type = check_only_one_dep(dependency, "branch", current_type, yaml_name)
433
+
434
+ unless current_type
435
+ log.error("#{yaml_name} - #{dependency['pod']}: path/podspec/version/git/tag/branch 字段请指定其中一个")
436
+ exit!
437
+ end
438
+
439
+ unless dependency['git']
440
+ if dependency['tag']
441
+ log.error("#{yaml_name} - #{dependency['pod']}: 你指定了tag,但未配置git地址,请为此组件配置git字段")
442
+ exit!
443
+ end
444
+ if dependency['branch']
445
+ log.error("#{yaml_name} - #{dependency['pod']}: 你指定了branch,但未配置git地址,请为此组件配置git字段")
446
+ exit!
447
+ end
448
+ end
449
+
450
+ # 检查数据类型
451
+ if dependency['configurations'] and dependency['configurations'].class != Array
452
+ log.error("#{yaml_name} - #{dependency['pod']} - #{dependency['configurations']}: configurations字段必须是数组,例如:['Debug','Release']")
453
+ exit!
454
+ end
455
+
456
+ if dependency['subspecs'] and dependency['subspecs'].class != Array
457
+ log.error("#{yaml_name} - #{dependency['pod']} - #{dependency['subspecs']}: subspecs字段必须是数组,例如:['A','B','C']")
458
+ exit!
459
+ end
460
+
461
+ # 校验自身文件重复
462
+ if dependencies_name.include?(dependency['pod'])
463
+ log.error("发现#{yaml_name}内重复组件:#{dependency['pod']},请检查并删除重复依赖")
464
+ exit!
465
+ else
466
+ dependencies_name << dependency['pod']
467
+ end
468
+
469
+ # 警告
470
+ if dependency['path'] and yaml_name != local_yaml_name
471
+ log.warn("组件#{dependency['pod']}使用path依赖方式应该写在#{local_yaml_name}")
472
+ end
473
+ if dependency['podspec'] and yaml_name != local_yaml_name
474
+ log.warn("组件#{dependency['pod']}使用podspec依赖方式应该写在#{local_yaml_name}")
475
+ end
476
+
477
+ end
478
+
479
+ def check_only_one_dep(dependency, key, current_type, yaml_name)
480
+ if dependency[key]
481
+ if current_type
482
+ log.error("#{yaml_name} - #{dependency['pod']}: #{current_type}字段和#{key}只能使用其中一个,请删除不需要的一个")
483
+ exit!
484
+ else
485
+ return key
486
+ end
487
+ end
488
+ current_type
489
+ end
490
+
491
+ def read_yaml_content(yaml_name)
492
+ unless File.exist?(yaml_name)
493
+ log.detail("文件不存在:#{yaml_name}")
494
+ return nil
495
+ end
496
+
497
+ begin
498
+ # aliases: true 支持YAML锚点变量(SOURCES上的 &binary/&source 锚点被组件的 *source 引用)
499
+ yaml_content = YAML.load_file(yaml_name, aliases: true)
500
+ rescue Exception => e
501
+ log.error(e.to_s)
502
+ log.error("#{yaml_name}文件解析异常, 请检查 ⬆️")
503
+ exit!
504
+ end
505
+
506
+ unless yaml_content.class == Hash
507
+ log.warn("#{yaml_name} => (文件不是keyValue形式)")
508
+ return nil
509
+ end
510
+
511
+ yaml_content
512
+ end
513
+
514
+ def print_targets_deps
515
+ log.step('读取依赖文件')
516
+ yaml_deps.each do |yaml_name, deps|
517
+ next if yaml_name == local_yaml_name
518
+ log.detail("#{yaml_name} => #{deps.size}个")
519
+ end
520
+ local_deps = yaml_deps[local_yaml_name]
521
+ log.detail("#{local_yaml_name} => #{local_deps ? local_deps.size : 0}个(本地覆盖)") if local_deps
522
+
523
+ targets_dependencies.each do |target, dependencies|
524
+ # 零依赖target只打印个数,不遍历具体依赖
525
+ log.step("target[#{target.name}] 解析后依赖共计#{dependencies.size}个")
526
+ next if dependencies.empty?
527
+
528
+ sort_deps = dependencies.sort do |dep1, dep2|
529
+ if dep1.type != dep2.type
530
+ dep1.type < dep2.type ? 1 : -1
531
+ else
532
+ dep1.pod.downcase <=> dep2.pod.downcase
533
+ end
534
+ end
535
+
536
+ # 默认值只在这里打印一次,与默认值相同的字段不再逐行输出
537
+ default_source = file_sources[target_yaml_name(target.name)]&.first
538
+ log.detail("以下字段使用默认值时逐行省略: :inhibit_warnings => true#{default_source ? ", :source => '#{default_source}'" : ''}")
539
+ log.info('')
540
+ sort_deps.each { |dependency|
541
+ # 依赖行缩进2空格,归属到当前target块
542
+ puts_result = " pod '#{dependency.pod}'"
543
+
544
+ if dependency.path #路径引用
545
+ puts_result += ", :path => '#{dependency.path}'"
546
+ elsif dependency.podspec #podspec引用
547
+ puts_result += ", :podspec => '#{dependency.podspec}'"
548
+ elsif dependency.version #版本号引用
549
+ puts_result += ", '#{dependency.version.to_s}'"
550
+ elsif dependency.git #git仓库引用
551
+ puts_result += ", :git => '#{dependency.git}'"
552
+ if dependency.tag
553
+ puts_result += ", :tag => '#{dependency.tag}'"
554
+ elsif dependency.branch
555
+ puts_result += ", :branch => '#{dependency.branch}'"
556
+ end
557
+ end
558
+
559
+ if dependency.configurations
560
+ puts_result += ", :configurations => ['#{dependency.configurations.join("','")}']"
561
+ end
562
+ if dependency.subspecs
563
+ puts_result += ", :subspecs => ['#{dependency.subspecs.join("','")}']"
564
+ end
565
+ # inhibit_warnings默认true,仅显式false时打印
566
+ puts_result += ", :inhibit_warnings => false" if dependency.inhibit_warnings == false
567
+ # source默认取SOURCES第一个,仅版本号引用且使用非默认源时打印
568
+ if dependency.version and dependency.source and dependency.source != default_source
569
+ puts_result += ", :source => '#{dependency.source}'"
570
+ end
571
+ if dependency.local_injected
572
+ puts_result += " [本地新增]"
573
+ elsif dependency.from_local
574
+ puts_result += " [本地覆盖]"
575
+ end
576
+
577
+ if dependency.path
578
+ branch = repo_branch(dependency.path)
579
+ modify = repo_modify(dependency.path)
580
+ # 路径依赖青色,分支名/仓库修改状态品红
581
+ log.info(colorize(puts_result, :cyan) + colorize(branch, :magenta) + colorize(modify, :magenta))
582
+
583
+ elsif dependency.branch
584
+ log.info(colorize(puts_result, :yellow))
585
+ else
586
+ log.info(puts_result)
587
+ end
588
+
589
+ }
590
+ end
591
+ end
592
+
593
+ # 检查工程里不存在对应target的孤儿依赖文件(target改名/删除后残留)
594
+ def check_orphan_yaml_files
595
+ expected = targets.map { |target| target_yaml_name(target.name) }
596
+ Dir.glob("*#{MyConstants::DEPENDENCIES_YAML_SUFFIX}").each do |file|
597
+ next if file == local_yaml_name
598
+ next if expected.include?(file)
599
+ # 文件名约定为 <TargetName>Dependencies.yaml,去掉后缀即配置对应的 target 名
600
+ target_name = File.basename(file, MyConstants::DEPENDENCIES_YAML_SUFFIX)
601
+ log.warn("检测到配置文件 #{file} 在工程目录中,但未检测到工程的target '#{target_name}',已忽略加载该配置文件。请检查该target是否存在并仍在使用,如不存在建议删除该配置文件,以免再次产生此警告")
602
+ end
603
+ end
604
+
605
+ # 生成默认yaml文件
606
+ def generate_yaml
607
+ generate_local_yaml
608
+ targets.each { |target| generate_target_yaml(target_yaml_name(target.name)) }
609
+ end
610
+
611
+ def generate_local_yaml
612
+ if File.exist?(local_yaml_name)
613
+ return
614
+ end
615
+ log.detail("生成#{local_yaml_name}文件")
616
+
617
+ content = ""
618
+ content = content + "# 本地依赖文件(不提交git),两种用途:\n"
619
+ content = content + "# 1. 按pod名覆盖任意 <TargetName>Dependencies.yaml 中的同名组件(通常写path做本地联调)\n"
620
+ content = content + "# 2. 直接引入target依赖文件未声明的新组件: 会自动加入主App target且不被裁剪;\n"
621
+ content = content + "# App扩展target不会自动注入,扩展需要的新组件请在对应target依赖文件显式声明\n"
622
+ content = content + "# 引用方式(path/podspec/tag/branch)与字段规则同target依赖文件\n"
623
+ content = content + "# inhibit_warnings 不写默认false,方便开发时看到本地库的警告并及时修改\n\n"
624
+
625
+ content = content + "# TRIM_UNUSED_POD为true时自动移除未被引用的依赖库,加速开发编译\n"
626
+ content = content + "TRIM_UNUSED_POD: false\n\n"
627
+
628
+ content = content + "# 默认只使用target依赖文件中SOURCES声明的私有源,不使用CocoaPods官方CDN trunk源\n"
629
+ content = content + "# 如本工程确需trunk,把下行改为true(仅本工程生效)\n"
630
+ content = content + "ENABLE_TRUNK: false\n\n"
631
+
632
+ content = content + "PODS: []\n\n"
633
+
634
+ content = content + "# 示例:\n"
635
+ content = content + "# - pod: Masonry\n"
636
+ content = content + "# path: ~/MasonryCodePath\n"
637
+ content = content + "# - pod: MJRefresh\n"
638
+ content = content + "# git: git@gitee.com:xxx/MJRefresh.git\n"
639
+ content = content + "# branch: master\n"
640
+
641
+ File.open(local_yaml_name, 'w') { |file|
642
+ file.write(content)
643
+ }
644
+ end
645
+
646
+ def generate_target_yaml(yaml_name)
647
+ if File.exist?(yaml_name)
648
+ return
649
+ end
650
+ log.detail("生成#{yaml_name}文件")
651
+
652
+ content = ""
653
+ content = content + "# #{File.basename(yaml_name, MyConstants::DEPENDENCIES_YAML_SUFFIX)} target的依赖清单(企业内部组件与第三方组件统一维护)\n"
654
+ content = content + "# 1、每个组件只能指定 version/tag/branch/path/podspec 其中一种引用方式(tag/branch需配合git)\n"
655
+ content = content + "# 2、path/podspec 本地联调依赖请写在#{local_yaml_name}中\n"
656
+ content = content + "# 3、组件不写source时默认使用SOURCES中的第一个源(通常把二进制源放第一位)\n"
657
+ content = content + "# 4、源地址可用YAML锚点定义为变量(&名字),组件下通过 *名字 引用,避免重复书写\n"
658
+ content = content + "# 5、inhibit_warnings 不写默认true(屏蔽远程库警告);configurations/subspecs 为数组且不能同时使用\n\n"
659
+
660
+ content = content + "SOURCES:\n"
661
+ content = content + "# - &binary 这里替换成默认specs源(一般是二进制源)\n"
662
+ content = content + "# - &source 这里替换成备选specs源(如源码源)\n\n"
663
+
664
+ content = content + "PODS: []\n\n"
665
+
666
+ content = content + "# 示例:\n"
667
+ content = content + "# - pod: Masonry # 默认源 + 版本号引用\n"
668
+ content = content + "# version: 1.1.0\n"
669
+ content = content + "# - pod: BSDebugPlugins # 用锚点引用源码源 + 仅Debug集成\n"
670
+ content = content + "# version: 1.0.0-SNAPSHOT\n"
671
+ content = content + "# source: *source\n"
672
+ content = content + "# configurations: [Debug]\n"
673
+ content = content + "# - pod: CocoaService # git分支引用\n"
674
+ content = content + "# git: git@gitee.com:xxx/CocoaService.git\n"
675
+ content = content + "# branch: master\n"
676
+
677
+ File.open(yaml_name, 'w') { |file|
678
+ file.write(content)
679
+ }
680
+ end
681
+
682
+ # 解析路径依赖仓库的真实.git目录。
683
+ # 普通仓库/.git是目录;submodule与worktree下.git是文件(内容为 gitdir: 实际git目录路径)
684
+ # @return [String, ""] 真实.git目录绝对路径,非git仓库返回""
685
+ def resolve_git_dir(path)
686
+ full_path = File.expand_path(path)
687
+ git_path = "#{full_path}/.git"
688
+ return "" unless Dir.exist?(git_path) || File.file?(git_path)
689
+
690
+ return git_path if Dir.exist?(git_path)
691
+
692
+ gitdir = File.read(git_path)[/gitdir:\s*(.+)/, 1]
693
+ return "" if gitdir.to_s.strip.empty?
694
+
695
+ File.expand_path(gitdir.strip, full_path)
696
+ end
697
+
698
+ # 获取某个目录下的当前分支
699
+ def repo_branch(path)
700
+ git_dir = resolve_git_dir(path)
701
+ return "" if git_dir.empty?
702
+
703
+ head_path = "#{git_dir}/HEAD"
704
+ unless File.exist?(head_path)
705
+ return ""
706
+ end
707
+
708
+ content = File.read(head_path)
709
+ content = content.gsub("\n", "")
710
+
711
+ branch_key_word = "ref: refs/heads/"
712
+ if content.include?(branch_key_word)
713
+ branch = content.gsub(branch_key_word, "")
714
+ return "(#{branch})"
715
+ end
716
+
717
+ if content.size >= 8
718
+ short_sha = content.slice(0, 7)
719
+ return "(#{short_sha})"
720
+ end
721
+
722
+ " #(#{content})"
723
+ end
724
+
725
+ # 获取仓库状态是否有修改
726
+ def repo_modify(path)
727
+ git_dir = resolve_git_dir(path)
728
+ return "" if git_dir.empty?
729
+
730
+ # git -C 在子进程中切换目录,避免全局Dir.chdir的副作用;
731
+ # 传仓库工作副本根目录,submodule/worktree由git自身通过.git文件定位真实git目录
732
+ status = %x(git -C #{Shellwords.escape(File.expand_path(path))} status -suno 2>/dev/null)
733
+ status.size > 0 ? "[modified]" : ""
734
+ end
735
+
736
+ # 读取主工程需要集成依赖的target列表
737
+ def init_main_targets
738
+ project_manager = XCProject::XcodeprojManager.share_manager
739
+ @targets = project_manager.get_app_targets.select { |target| integratable_target?(target) }
740
+ end
741
+
742
+ # 是否裁剪未被引用的依赖库(加速编译)
743
+ # 单次选项 pod install/update --trim/--full 优先于 LocalDependencies.yaml 的 TRIM_UNUSED_POD
744
+ def trim_unused_pod?
745
+ yaml_content = read_yaml_content(local_yaml_name)
746
+ return $trim_unused_pod unless $trim_unused_pod.nil?
747
+ yaml_content ? !!(yaml_content["TRIM_UNUSED_POD"]) : false
748
+ end
749
+
750
+ #是否禁用检查podspec文件
751
+ def disable_check_podspec
752
+ yaml_content = read_yaml_content(local_yaml_name)
753
+ disable_check_podspec = yaml_content ? !!(yaml_content["DISABLE_CHECK_PODSPEC"]) : false
754
+ if disable_check_podspec
755
+ log.warn("已关闭podspec文件检查,如需开启,请在#{local_yaml_name}里设置DISABLE_CHECK_PODSPEC为false")
756
+ end
757
+ disable_check_podspec
758
+ end
759
+
760
+ private
761
+
762
+ # 统一日志入口
763
+ def log
764
+ CocoaPods::Yaml.log
765
+ end
766
+
767
+ # 仅TTY下对着色,避免CI/重定向日志出现ANSI转义码
768
+ def colorize(text, color)
769
+ return text.to_s if text.to_s.empty?
770
+ $stdout.tty? ? text.to_s.public_send(color) : text.to_s
771
+ end
772
+
773
+ end
774
+ end