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,195 @@
1
+ require_relative 'project'
2
+ require_relative 'util'
3
+ require_relative '../my_constants'
4
+
5
+ module Xcodeproj
6
+ class PodspecChecker
7
+
8
+ # @return [XCodeProject]
9
+ attr_accessor :project
10
+
11
+ # @param project::[XCodeProject]
12
+ def initialize(project)
13
+ @project = project
14
+ end
15
+
16
+ # 检查podspec文件
17
+ def check_podspec
18
+
19
+ yaml_dep = Pod::YamlDependencies.new
20
+ if yaml_dep.disable_check_podspec
21
+ return
22
+ end
23
+
24
+ CocoaPods::Yaml.log.step("podspec文件检查中...")
25
+ start = (Time.now.to_f * 1000).to_i
26
+ module_count = 0
27
+
28
+ project.module_items.each do |module_name, module_item|
29
+ unless module_item.is_development_module
30
+ next
31
+ end
32
+ unless module_item.podspec_path
33
+ next
34
+ end
35
+ module_count += 1
36
+
37
+ module_item.init_absolute_import_info
38
+
39
+ # 代码里需要的模块
40
+ need_modules = get_code_need_modules(module_item)
41
+
42
+ # podspec直接依赖
43
+ podspec_modules = get_podspec_modules(module_item)
44
+
45
+ loss = loss_modules(need_modules, podspec_modules)
46
+ unused = unused_modules(need_modules, podspec_modules, module_item)
47
+
48
+ if loss.size >0
49
+ CocoaPods::Yaml.log.warn("组件#{module_item.module_name}可能缺少以下依赖(#{loss.size}个), 请检查:")
50
+ loss.sort.each do |name|
51
+ CocoaPods::Yaml.log.info(" s.dependency '#{name}'")
52
+ end
53
+ end
54
+
55
+ if unused.size >0
56
+ CocoaPods::Yaml.log.warn("组件#{module_item.module_name}可能未使用以下依赖(#{unused.size}个), 请检查:")
57
+ unused.sort.each { |name| CocoaPods::Yaml.log.info(" #{name}") }
58
+ end
59
+
60
+ end
61
+
62
+ duration = ((Time.now.to_f * 1000).to_i - start)*0.001
63
+ CocoaPods::Yaml.log.detail("podspec检查完毕! 共检查#{module_count}个组件 耗时:#{duration.round(2)}秒")
64
+ end
65
+
66
+ private
67
+
68
+ # podspec.json中平台级配置使用的键,s.ios.dependency等会序列化到这些键下
69
+ PLATFORM_KEYS = %w[ios osx tvos watchos visionos].freeze
70
+
71
+ # 获取缺失的模块
72
+ # @param need_modules::[String]
73
+ # @param podspec_modules::[Hash{String => [String]}]
74
+ def loss_modules(need_modules, podspec_modules)
75
+ # 一次性展开所有podspec声明的framework/组件名
76
+ declared_modules = podspec_modules.values.flatten.uniq
77
+
78
+ need_modules.select do |name|
79
+ !declared_modules.include?(name) && module_is_installed(name)
80
+ end
81
+ end
82
+
83
+ # 获取多余的模块
84
+ # @param need_modules::[String]
85
+ # @param podspec_modules::[Hash{String => [String]}]
86
+ def unused_modules(need_modules, podspec_modules, module_item)
87
+ res = []
88
+ podspec_modules.each do |dep_name, array|
89
+ contain = false
90
+ array.each do |vendor|
91
+ if need_modules.include?(vendor)
92
+ contain = true
93
+ end
94
+ end
95
+
96
+ if not contain and module_item.module_name != dep_name
97
+ res << dep_name
98
+ end
99
+ end
100
+ res
101
+ end
102
+
103
+ # @note 获取代码里需要的模块
104
+ def get_code_need_modules(module_item)
105
+ import_items = module_item.absolute_import_items
106
+ result_dic = {}
107
+ import_items.each do |import_item|
108
+ # 过滤系统库
109
+ if system_modules.include?(import_item.belong_module)
110
+ next
111
+ end
112
+
113
+ # 过滤当前库
114
+ if module_item.module_name == import_item.belong_module
115
+ next
116
+ end
117
+
118
+ result_dic[import_item.belong_module] = import_item
119
+ end
120
+ result_dic.keys
121
+ end
122
+
123
+ # 获取podspec文件里写的模块 直接依赖
124
+ # @return [Hash{String => [String]}]
125
+ def get_podspec_modules(module_item)
126
+
127
+ podspec_content = Util.get_podspec_content(module_item.podspec_path)
128
+
129
+ # 递归收集当前层、所有嵌套subspec、以及平台级(ios/osx等)声明的依赖
130
+ podspec_dependencies = []
131
+ collect_podspec_dependencies(podspec_content, podspec_dependencies)
132
+
133
+ # 转成map
134
+ result = {}
135
+ podspec_dependencies.each do |podspec_dependency|
136
+ podspec_dependency = podspec_dependency.split("/")[0] #如果podspec里写的依赖是带/的,取/前第一个, 例如:s.dependency 'UMCShare/Social/ReducedQQ'
137
+ dependency_item = project.module_items[podspec_dependency]
138
+ if dependency_item
139
+ result[podspec_dependency] = dependency_item.frameworks
140
+ else #如果/都处理了,还是取不到,那就整个默认值,自身
141
+ frameworks = [podspec_dependency]
142
+ if podspec_dependency.include?("/") #处理依赖 s.dependency 'UMCShare/Social/ReducedQQ'
143
+ frameworks << podspec_dependency.split("/")[0]
144
+ end
145
+ result[podspec_dependency] = frameworks
146
+ end
147
+ end
148
+
149
+ result
150
+ end
151
+
152
+ # 递归收集podspec(含嵌套subspec、平台级配置)中声明的全部依赖名
153
+ # @param spec_content::[Hash] podspec.json解析出的当前层内容
154
+ # @param dependencies::[Array<String>] 累加结果
155
+ def collect_podspec_dependencies(spec_content, dependencies)
156
+ return unless spec_content.is_a?(Hash)
157
+
158
+ if spec_content["dependencies"].is_a?(Hash)
159
+ dependencies.concat(spec_content["dependencies"].keys)
160
+ end
161
+
162
+ # s.ios.dependency 'X' 等平台级依赖会序列化到 ios/osx/... 键下
163
+ PLATFORM_KEYS.each do |platform_key|
164
+ platform_content = spec_content[platform_key]
165
+ collect_podspec_dependencies(platform_content, dependencies) if platform_content.is_a?(Hash)
166
+ end
167
+
168
+ # 嵌套subspec递归收集
169
+ subspecs = spec_content["subspecs"]
170
+ if subspecs.is_a?(Array)
171
+ subspecs.each do |subspec|
172
+ collect_podspec_dependencies(subspec, dependencies)
173
+ end
174
+ end
175
+ end
176
+
177
+ # 判断某个库是否被安装
178
+ def module_is_installed(module_name)
179
+ all = project.lock_content["DEPENDENCIES"]
180
+ unless all
181
+ return false
182
+ end
183
+ # 使用非破坏式map生成归一化数组,禁止collect!改写lock原始数据
184
+ normalized = all.map do |dep|
185
+ dep.split(" (")[0].split("/")[0]
186
+ end
187
+ normalized.include?(module_name)
188
+ end
189
+ # 系统级的依赖库
190
+ # @return [String]
191
+ def system_modules
192
+ MyConstants::LIBLIBARYS + MyConstants::FRAMEWORKS
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,405 @@
1
+ require 'pathname'
2
+ require 'colored2'
3
+ require 'cocoapods/downloader/request'
4
+ require_relative 'xcodeproj'
5
+ require_relative 'util'
6
+ require_relative 'item'
7
+
8
+ module Xcodeproj
9
+
10
+ class XCodeProject
11
+
12
+ # 参与import/podspec分析的源码后缀
13
+ CODE_FILE_EXTENSIONS = %w[.h .m .mm .pch .c .cc .cpp .hpp].freeze
14
+ HEADER_FILE_EXTENSIONS = %w[.h .hpp].freeze
15
+
16
+ # 壳工程xcodeproj对象
17
+ # @return Xcodeproj::Project
18
+ attr_accessor :shell_project
19
+
20
+ # Pods工程xcodeproj对象
21
+ # @return Xcodeproj::Project
22
+ attr_accessor :pod_project
23
+
24
+ # @return [Hash{String => ModuleItem}]
25
+ attr_accessor :module_items
26
+
27
+ # @return [Hash]
28
+ attr_accessor :lock_content
29
+
30
+ # @return [Hash]
31
+ attr_accessor :version_map
32
+
33
+ def initialize
34
+ @shell_project = nil
35
+ @pod_project = nil
36
+ @module_items = {}
37
+ @version_map = {}
38
+ @lock_content = {}
39
+ init_shell_project
40
+ init_pod_project
41
+ init_lockfile
42
+ init_version_map
43
+ expand_module
44
+ init_vendor_frameworks
45
+ end
46
+
47
+ def init_shell_project
48
+ # 统一走XcodeprojManager的壳工程查找逻辑,避免多份实现选取结果不一致
49
+ @shell_project = XCProject::XcodeprojManager.share_manager.get_shell_project
50
+ end
51
+
52
+ def init_pod_project
53
+ xcodeproj_path = "#{Dir.pwd}/Pods/Pods.xcodeproj"
54
+ unless File.exist?(xcodeproj_path)
55
+ return nil
56
+ end
57
+
58
+ @pod_project = Xcodeproj::Project.open(xcodeproj_path)
59
+ end
60
+
61
+ def init_lockfile
62
+ lockfile_path = MyConstants::PODFILE_LOCK
63
+
64
+ unless File.exist?(lockfile_path)
65
+ return
66
+ end
67
+
68
+ @lock_content = YAML.load_file(lockfile_path)
69
+ end
70
+ def init_version_map
71
+ lock_content = @lock_content
72
+
73
+ remote_dependencies = lock_content["PODS"]
74
+ unless remote_dependencies
75
+ return
76
+ end
77
+
78
+ remote_dependencies.each do |dependency|
79
+ if dependency.class == Hash
80
+ dependency = dependency.keys.first
81
+ end
82
+ # 外部源条目形如 "Xxx (from `path`)",没有版本号;
83
+ # 不跳过会把 "from `path`" 当版本存入,污染get_podspec_path的缓存路径推断
84
+ next if dependency.include?("(from")
85
+
86
+ temp = dependency.split("(")
87
+ # 无括号段(理论上不出现)时temp[1]为nil,直接跳过避免NoMethodError
88
+ version = temp[1]
89
+ next if version.nil?
90
+
91
+ name = temp[0].split("/")[0] # UMCShare/Social/ReducedWeChat
92
+ name = name.gsub(" ", "").gsub(")", "")
93
+ version = version.gsub("= ", "").gsub(")", "")
94
+ next if version.empty?
95
+
96
+ @version_map[name] = version
97
+ end
98
+ end
99
+
100
+ # 展开模块
101
+ def expand_module
102
+ self.expand_with_module(pod_project.main_group, nil)
103
+ end
104
+
105
+ # 展开模块
106
+ def expand_with_module(group, module_name)
107
+
108
+ if group.class != Xcodeproj::Project::Object::PBXGroup
109
+ return
110
+ end
111
+
112
+ if not module_name and group.hierarchy_path
113
+ group_hierarchy_path_array = group.hierarchy_path.split("/")
114
+ module_name = group_hierarchy_path_array.size >= 3 ? group_hierarchy_path_array[2] : nil
115
+ end
116
+
117
+ # 创建 source_file_item
118
+ source_file_item = @module_items[module_name]
119
+ if module_name and not source_file_item
120
+ source_file_item = ModuleItem.new(module_name)
121
+ source_file_item.podspec_path = get_podspec_path(module_name)
122
+ @module_items[module_name] = source_file_item
123
+ end
124
+
125
+ # 更新module_path
126
+ if source_file_item and not source_file_item.module_path and group.hierarchy_path
127
+ group_hierarchy_path_array = group.hierarchy_path.split("/")
128
+ if group_hierarchy_path_array.size == 3
129
+ source_file_item.module_path = group.real_path
130
+ end
131
+ if group_hierarchy_path_array.include?("Development Pods")
132
+ source_file_item.is_development_module = true
133
+ end
134
+ end
135
+
136
+ # 文件引用
137
+ file_reference_class = Xcodeproj::Project::Object::PBXFileReference
138
+ # PBXFileSystemSynchronizedRootGroup在xcodeproj 1.25(Xcode16)才引入,旧版本取nil走普通分组逻辑
139
+ synchronized_group_class =
140
+ if Xcodeproj::Project::Object.const_defined?(:PBXFileSystemSynchronizedRootGroup)
141
+ Xcodeproj::Project::Object.const_get(:PBXFileSystemSynchronizedRootGroup)
142
+ end
143
+
144
+ not_child_names = ["Podfile","Frameworks", "Products","Targets Support Files","Support Files"]
145
+ source_file_extension = CODE_FILE_EXTENSIONS
146
+ header_file_extension = HEADER_FILE_EXTENSIONS
147
+
148
+ group.children.each {|child|
149
+
150
+ module_name = source_file_item ? source_file_item.module_name : nil
151
+
152
+ # Xcode16+的文件系统同步分组不是PBXGroup子类(PBXFileSystemSynchronizedRootGroup
153
+ # 直接继承AbstractObject),既没有name也没有real_path,走普通递归会在方法入口被整体跳过,
154
+ # 导致该组件不进module_items,podspec检查和import检查会静默失效。
155
+ # 该判断必须在child.name过滤之前(同步分组不响应name方法)。这里改为按磁盘目录扫描展开。
156
+ if synchronized_group_class && child.is_a?(synchronized_group_class)
157
+ expand_synchronized_group(group, child, module_name)
158
+ next
159
+ end
160
+
161
+ if not_child_names.include?(child.name)
162
+ # puts "不检查:#{child.name} #{child.real_path}"
163
+ next
164
+ end
165
+
166
+ if child.class != file_reference_class
167
+ # puts "不是文件引用:#{child.class} #{child.real_path}"
168
+ self.expand_with_module(child, module_name)
169
+ next
170
+ end
171
+
172
+ hierarchy_path_array = child.hierarchy_path.split("/")
173
+ if hierarchy_path_array.size < 3
174
+ self.expand_with_module(child, module_name)
175
+ next
176
+ end
177
+
178
+ if source_file_item
179
+ extend_name = child.real_path.basename.extname
180
+ basename = child.real_path.basename
181
+ if basename.to_s == "#{module_name}.podspec" or basename.to_s == "#{module_name}.podspec.json"
182
+ source_file_item.podspec_path = child.real_path
183
+ elsif source_file_extension.include?(extend_name)
184
+ source_file_item.source_files[child.real_path.basename.to_s] = child.real_path
185
+ if header_file_extension.include?(extend_name)
186
+ source_file_item.header_files[child.real_path.basename.to_s] = child.real_path
187
+ end
188
+ else
189
+ # puts child.real_path
190
+ end
191
+ end
192
+
193
+ # 递归展开
194
+ self.expand_with_module(child, module_name)
195
+ }
196
+
197
+ end
198
+
199
+ # 展开Xcode16+文件系统同步分组(PBXFileSystemSynchronizedRootGroup)
200
+ # 同步分组不在工程文件里列举成员,磁盘上有什么文件Xcode就编译什么,
201
+ # 因此这里直接扫描磁盘目录收集源码与podspec,语义与普通分组遍历保持一致。
202
+ # 注意: 同步分组没有hierarchy_path/real_path/name,层级信息需从父分组拼接
203
+ # @param parent_group [PBXGroup] 承载该同步分组的普通分组(用于解析相对路径)
204
+ # @param sync_group [PBXFileSystemSynchronizedRootGroup]
205
+ # @param module_name [String, nil] 父分组已解析出的组件名
206
+ def expand_synchronized_group(parent_group, sync_group, module_name)
207
+ # 与普通分组一致,按层级路径解析组件名
208
+ hierarchy_path = build_synchronized_hierarchy_path(parent_group, sync_group)
209
+ if hierarchy_path
210
+ hierarchy_path_array = hierarchy_path.split("/")
211
+ module_name = hierarchy_path_array[2] if hierarchy_path_array.size >= 3
212
+ end
213
+
214
+ source_file_item = @module_items[module_name]
215
+ if module_name && source_file_item.nil?
216
+ source_file_item = ModuleItem.new(module_name)
217
+ source_file_item.podspec_path = get_podspec_path(module_name)
218
+ @module_items[module_name] = source_file_item
219
+ end
220
+ return unless source_file_item
221
+
222
+ disk_path = synchronized_disk_path(parent_group, sync_group)
223
+ return unless disk_path && File.directory?(disk_path)
224
+
225
+ # 与普通分组逻辑一致: 层级深度为3的分组对应组件根目录
226
+ if source_file_item.module_path.nil? && hierarchy_path && hierarchy_path.split("/").size == 3
227
+ source_file_item.module_path = Pathname.new(disk_path)
228
+ end
229
+ if hierarchy_path&.split("/")&.include?("Development Pods")
230
+ source_file_item.is_development_module = true
231
+ end
232
+
233
+ scan_disk_module_files(source_file_item, disk_path, module_name)
234
+ end
235
+
236
+ # 拼接同步分组的层级路径(其自身不响应hierarchy_path)
237
+ # @return [String, nil] 形如 "/Development Pods/BSLogin"
238
+ def build_synchronized_hierarchy_path(parent_group, sync_group)
239
+ parent_path = parent_group.respond_to?(:hierarchy_path) ? parent_group.hierarchy_path : nil
240
+ return nil if parent_path.nil?
241
+
242
+ "#{parent_path}/#{sync_group.display_name}"
243
+ end
244
+
245
+ # 递归收集磁盘目录下的源码文件和podspec(对应Xcode16+同步文件夹分组)
246
+ # @param module_item [ModuleItem]
247
+ # @param disk_path [String, Pathname] 同步分组在磁盘上的根目录
248
+ # @param module_name [String]
249
+ def scan_disk_module_files(module_item, disk_path, module_name)
250
+ Dir.glob(File.join(disk_path.to_s, '**', '*')).each do |path|
251
+ next unless File.file?(path)
252
+
253
+ basename = File.basename(path)
254
+ extend_name = File.extname(path)
255
+ pathname = Pathname.new(path)
256
+ if basename == "#{module_name}.podspec" || basename == "#{module_name}.podspec.json"
257
+ module_item.podspec_path = pathname
258
+ elsif CODE_FILE_EXTENSIONS.include?(extend_name)
259
+ module_item.source_files[basename] = pathname
260
+ if HEADER_FILE_EXTENSIONS.include?(extend_name)
261
+ module_item.header_files[basename] = pathname
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ # 计算同步分组在磁盘上的绝对路径
268
+ # @param parent_group [PBXGroup] 承载该同步分组的普通分组(有real_path)
269
+ # @param sync_group [PBXFileSystemSynchronizedRootGroup] 只有相对path,没有real_path
270
+ def synchronized_disk_path(parent_group, sync_group)
271
+ relative = sync_group.path
272
+ return nil if relative.nil? || relative.empty?
273
+
274
+ path = Pathname.new(relative)
275
+ return path if path.absolute?
276
+
277
+ case sync_group.source_tree
278
+ when 'SOURCE_ROOT'
279
+ # 相对Pods工程源根目录,不需要父分组回退
280
+ base = pod_project.root_object.main_group.real_path
281
+ return path unless base
282
+
283
+ base.join(relative)
284
+ else # '<group>'(默认): 相对所在父分组
285
+ base = parent_group.real_path
286
+ candidate = base ? base.join(relative) : path
287
+ # 部分工程父分组未配置显式path,real_path只解析到工程根目录,
288
+ # 此时按分组名再补一层做回退(Development Pods等分组名通常与磁盘目录同名)
289
+ if base && !candidate.directory? && parent_group.respond_to?(:display_name)
290
+ fallback = base.join(parent_group.display_name.to_s, relative)
291
+ candidate = fallback if fallback.directory?
292
+ end
293
+ candidate
294
+ end
295
+ end
296
+
297
+ # 读取某个模块的podspec路径
298
+ def get_podspec_path(module_name)
299
+ check_options = lock_content["CHECKOUT OPTIONS"]
300
+ if check_options and check_options[module_name]
301
+ check_option = check_options[module_name]
302
+ request = Pod::Downloader::Request.new(released:false, name:module_name, params:check_option)
303
+ slug_opts = {}
304
+ cache_path = request.slug(**slug_opts)
305
+ return "~/Library/Caches/CocoaPods/Pods/Specs/#{cache_path}.podspec.json"
306
+ end
307
+
308
+ # :path: localPods/XX
309
+ local_spec = lock_content["EXTERNAL SOURCES"]
310
+ if local_spec and local_spec[module_name]
311
+ path = local_spec[module_name][:path]
312
+ podspec_path = "#{path}/#{module_name}.podspec"
313
+ podspec_json_path = "#{path}/#{module_name}.podspec.json"
314
+ if File.exist?(podspec_path)
315
+ return Pathname(podspec_path).expand_path
316
+ elsif File.exist?(podspec_json_path)
317
+ return Pathname(podspec_json_path).expand_path
318
+ else
319
+ return nil
320
+ end
321
+ end
322
+
323
+ # 4.0.1-24ee2.podspec.json
324
+ version_spec = @lock_content["SPEC CHECKSUMS"]
325
+ md5_to5 = ""
326
+ if version_spec and version_spec[module_name]
327
+ md5 = version_spec[module_name]
328
+ md5_to5 = md5.slice(0,5)
329
+ end
330
+ version = @version_map[module_name]
331
+ if version
332
+ podspec_json_name = "#{version}-#{md5_to5}.podspec.json"
333
+ path = "~/Library/Caches/CocoaPods/Pods/Specs/Release/#{module_name}/#{podspec_json_name}"
334
+ return Pathname(path).expand_path
335
+ end
336
+
337
+ nil
338
+ end
339
+
340
+
341
+ # 初始化模块的vendor_frameworks
342
+ def init_vendor_frameworks
343
+ @module_items.each_value do |module_item|
344
+ module_item.frameworks << module_item.module_name
345
+
346
+ unless module_item.podspec_path
347
+ CocoaPods::Yaml.log.warn("读取#{module_item.module_name}的podspec路径为空,请发送Podfile.lock文件联系开发者修复")
348
+ next
349
+ end
350
+
351
+ podspec_content = Util.get_podspec_content(module_item.podspec_path)
352
+
353
+ unless podspec_content
354
+ next
355
+ end
356
+
357
+ update_dep_frameworks(module_item.frameworks, podspec_content)
358
+
359
+ subspec_content = podspec_content["subspecs"]
360
+ if subspec_content and subspec_content.class == Array
361
+ subspec_content.each { |json_content|
362
+ update_dep_frameworks(module_item.frameworks, json_content)
363
+ }
364
+ end
365
+ end
366
+
367
+ end
368
+
369
+ def update_dep_frameworks(item_frameworks, json_content)
370
+ vendor_frameworks = json_content["vendored_frameworks"]
371
+ unless vendor_frameworks
372
+ ios = json_content["ios"]
373
+ if ios and ios.class == Hash
374
+ vendor_frameworks = ios["vendored_frameworks"]
375
+ end
376
+ end
377
+
378
+ if vendor_frameworks
379
+ if vendor_frameworks.class == Array
380
+ vendor_frameworks.each do |vendor_framework|
381
+ frameworks = deal_frameworks(vendor_framework)
382
+ if frameworks != "*"
383
+ item_frameworks << frameworks
384
+ end
385
+ end
386
+ elsif vendor_frameworks.class == String
387
+ frameworks = deal_frameworks(vendor_frameworks)
388
+ if frameworks != "*"
389
+ item_frameworks << frameworks
390
+ end
391
+ end
392
+ end
393
+ end
394
+
395
+ def deal_frameworks(frameworks)
396
+ # "frameworks/WCDB/WCDB.framework",
397
+ # "openssl.xcframework"
398
+ temp_array = frameworks.split("/")
399
+ result = temp_array.last
400
+ result = result.gsub(".xcframework","")
401
+ result.gsub(".framework", "")
402
+ end
403
+
404
+ end
405
+ end