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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colored2'
4
+
5
+ module CocoaPods
6
+ module Yaml
7
+ # 统一日志,替代散落各处的 puts/warn。
8
+ #
9
+ # 级别:
10
+ # step 阶段标题(⏩,前置空行)
11
+ # info 普通信息(默认色)
12
+ # detail 细节说明/阶段性完成(-> 前缀);流程内各步骤完成统一用此级别
13
+ # success 成功提示(✅,绿色);仅用于整条命令/流程的最终关键结果,阶段步骤不要用,避免满屏✅
14
+ # warn 警告(⚠️,黄色,不中断流程)
15
+ # error 错误(❌,红色;是否退出由调用方决定)
16
+ # debug 调试细节(· 前缀,仅 pod install --verbose 或 COCOAPODS_YAML_VERBOSE 非空时输出)
17
+ #
18
+ # 说明: warn/error也输出到stdout而非stderr。本插件运行在CocoaPods进程内,
19
+ # 分流到stderr会被CocoaPods自身输出缓冲穿插,导致警告标题与其明细行错位。
20
+ # 仅当输出流是TTY时才着色,避免CI/重定向日志中出现ANSI转义码。
21
+ class Logger
22
+ DEBUG_ENV = 'COCOAPODS_YAML_VERBOSE'
23
+
24
+ def step(message)
25
+ write($stdout, "\n⏩ #{message}", :cyan, true)
26
+ end
27
+
28
+ def info(message = '', color = nil)
29
+ write($stdout, message.to_s, color)
30
+ end
31
+
32
+ def detail(message)
33
+ write($stdout, "-> #{message}", :blue)
34
+ end
35
+
36
+ def success(message)
37
+ write($stdout, "✅ #{message}", :green)
38
+ end
39
+
40
+ def warn(message)
41
+ write($stdout, "⚠️ #{message}", :yellow)
42
+ end
43
+
44
+ def error(message)
45
+ write($stdout, "❌ #{message}", :red)
46
+ end
47
+
48
+ def debug(message)
49
+ write($stdout, "· #{message}", nil) if debug_enabled?
50
+ end
51
+
52
+ def debug_enabled?
53
+ return true unless ENV.fetch(DEBUG_ENV, '').strip.empty?
54
+ if defined?(Pod::Config)
55
+ begin
56
+ return !!Pod::Config.instance.verbose?
57
+ rescue StandardError
58
+ false
59
+ end
60
+ end
61
+ false
62
+ end
63
+
64
+ private
65
+
66
+ # @param io [IO] 输出流
67
+ # @param message [String] 内容
68
+ # @param color [Symbol, nil] 颜色方法(colored2),nil不着色
69
+ # @param bold [Boolean] 是否加粗
70
+ def write(io, message, color, bold = false)
71
+ text = message
72
+ if color && io.respond_to?(:tty?) && io.tty?
73
+ text = text.public_send(color)
74
+ text = text.bold if bold
75
+ end
76
+ io.puts(text)
77
+ end
78
+ end
79
+
80
+ # @return [CocoaPods::Yaml::Logger] 全局日志实例
81
+ def self.log
82
+ @log ||= Logger.new
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'find'
5
+ require 'pathname'
6
+
7
+ module ModifyCode
8
+ # 三方库源码编译兼容补丁
9
+ #
10
+ # 在 pod install/update 的 post_install 阶段,对集成进工程的三方库源码做
11
+ # 不改变功能的微调,使其能在新版 Xcode 上编译。放在 cocoapods-yaml 中执行是为了
12
+ # 同时覆盖两种使用场景,避免“打包机能编译、开发者源码接入却编译失败”:
13
+ # 1. 日常开发的普通 pod 源码接入(Pods/<组件名>)
14
+ # 2. pod-binary-pack 打包期的 :path 开发 pod(源码目录可能在 Pods 之外)
15
+ # 二进制 pod 目录下仅含 .framework/.xcframework,遍历源码时会剪枝跳过,绝不改动已构建产物
16
+ class SourcePatcher
17
+ # 可编译源码后缀
18
+ SOURCE_SUFFIX = %w[.h .m .mm .c .cpp].freeze
19
+ # 设置该环境变量可临时关闭全部源码兼容补丁
20
+ DISABLE_ENV = 'COCOAPODS_YAML_SKIP_SOURCE_PATCH'
21
+
22
+ # 一条兼容规则
23
+ # pod: 组件名;nil 表示对所有组件的源码生效(规则本身必须安全通用)
24
+ # files: 相对组件源码根的精确文件路径数组;nil 表示遍历全部源码文件
25
+ # pattern: String 时字面匹配,Regexp 时可用 \1 \2 反向引用
26
+ # replacement 配合 pattern 使用,日志标签 label 便于定位是哪条兼容规则
27
+ Rule = Struct.new(:pod, :files, :pattern, :replacement, :label, keyword_init: true) do
28
+ def global?
29
+ pod.nil?
30
+ end
31
+ end
32
+
33
+ # 规则按声明顺序执行;新增同类兼容处理时在此追加一条即可,不要把逻辑散落各处
34
+ RULES = [
35
+ # 通用规则:新版 Xcode(16+) SDK 将 netinet6/in6.h 划为 Darwin 模块私有头,
36
+ # 且该头内部 #error 明确要求改引公共头 netinet/in.h(AFNetworking 4.0.1 等旧库因此编译失败)
37
+ Rule.new(
38
+ pod: nil,
39
+ files: nil,
40
+ pattern: /^(\s*#\s*(?:import|include)\s*[<"])netinet6\/in6\.h([>"])/,
41
+ replacement: '\\1netinet/in.h\\2',
42
+ label: 'netinet6/in6.h 已废弃,改用公共头 netinet/in.h'
43
+ ),
44
+
45
+ # YYText 1.0.7:命中字符簇/emoji 内部时按距离远近吸附到左右边界,
46
+ # 原代码把三元运算符误写成链式比较 a < b < c,Xcode 16+ 将 -Wparentheses 升级为编译错误。
47
+ # 同文件内垂直/水平两种写法各出现 2 次,String 精确替换全部命中
48
+ Rule.new(
49
+ pod: 'YYText',
50
+ files: ['YYText/Component/YYTextLayout.m'],
51
+ pattern: 'position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next);',
52
+ replacement: 'position = fabs(left - point.y) < fabs(right - point.y) ? prev : next;',
53
+ label: '修复链式比较 X<Y<Z 为最近边界三元选择'
54
+ ),
55
+ Rule.new(
56
+ pod: 'YYText',
57
+ files: ['YYText/Component/YYTextLayout.m'],
58
+ pattern: 'position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next);',
59
+ replacement: 'position = fabs(left - point.x) < fabs(right - point.x) ? prev : next;',
60
+ label: '修复链式比较 X<Y<Z 为最近边界三元选择'
61
+ ),
62
+
63
+ # FBRetainCycleDetector 历史编译兼容(原 modify_fb 逻辑原样迁移为规则)
64
+ Rule.new(
65
+ pod: 'FBRetainCycleDetector',
66
+ files: ['FBRetainCycleDetector/Layout/Classes/FBClassStrongLayout.mm'],
67
+ pattern: 'layoutCache[currentClass] = ivars;',
68
+ replacement: 'layoutCache[(id<NSCopying>)currentClass] = ivars;',
69
+ label: '修复新版 Xcode 下 layoutCache 下标类型不匹配'
70
+ ),
71
+ Rule.new(
72
+ pod: 'FBRetainCycleDetector',
73
+ files: ['fishhook/fishhook.c'],
74
+ pattern: 'indirect_symbol_bindings[i] = cur->rebindings[j].replacement;',
75
+ replacement: 'if(i < (sizeof(indirect_symbol_bindings) /sizeof(indirect_symbol_bindings[0]))) {indirect_symbol_bindings[i]=cur->rebindings[j].replacement;}',
76
+ label: '修复 fishhook 数组越界风险'
77
+ ),
78
+ Rule.new(
79
+ pod: 'FBRetainCycleDetector',
80
+ files: ['FBRetainCycleDetector/Graph/FBObjectiveCGraphElement.mm'],
81
+ pattern: '#import <objc/runtime.h>',
82
+ replacement: "#import <objc/runtime.h>\n#import <malloc/malloc.h>",
83
+ label: '补充 malloc/malloc.h 头引用'
84
+ ),
85
+ Rule.new(
86
+ pod: 'FBRetainCycleDetector',
87
+ files: ['FBRetainCycleDetector/Graph/FBObjectiveCGraphElement.mm'],
88
+ pattern: 'Class aCls = object_getClass(object);',
89
+ replacement: 'malloc_zone_t *zone = malloc_zone_from_ptr((__bridge void *)object); if (zone) { Class aCls=object_getClass(object);',
90
+ label: 'malloc_zone 增加非空保护'
91
+ ),
92
+ Rule.new(
93
+ pod: 'FBRetainCycleDetector',
94
+ files: ['FBRetainCycleDetector/Graph/FBObjectiveCGraphElement.mm'],
95
+ pattern: '#endif',
96
+ replacement: "}\n# endif",
97
+ label: '补齐 malloc_zone 非空保护的闭合括号'
98
+ )
99
+ ].freeze
100
+
101
+ # post_install 钩子入口
102
+ # @param sandbox [Pod::Sandbox] post_install 上下文中的沙盒
103
+ def self.patch(sandbox)
104
+ return if ENV.key?(DISABLE_ENV)
105
+ return unless sandbox
106
+
107
+ new(sandbox).patch
108
+ end
109
+
110
+ def initialize(sandbox)
111
+ @sandbox = sandbox
112
+ end
113
+
114
+ def patch
115
+ roots = pod_source_roots
116
+ return if roots.empty?
117
+
118
+ RULES.each do |rule|
119
+ targets = rule.global? ? roots : roots.select { |name, _| name == rule.pod }
120
+ targets.each do |pod_name, root|
121
+ hit_files = apply_rule(rule, pod_name, root)
122
+ # 兼容补丁可能改变三方库原始行为,实际命中的文件逐条用警告级别记录,
123
+ # 确保默认日志可见、有迹可查;无改动则完全静默
124
+ hit_files.each do |relative|
125
+ CocoaPods::Yaml.log.warn("源码兼容补丁[#{rule.label}]:#{pod_name}/#{relative}")
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ # 收集本次安装实际存在源码的组件根目录:开发 pod(:path) + 普通 pod(Pods/<名>)
134
+ # @return [Hash{String => String}] 组件名 => 源码根绝对路径
135
+ def pod_source_roots
136
+ roots = {}
137
+
138
+ # 组件名取 Manifest.lock 实际安装列表 + 开发 pod(:path)列表(后者兜底 Manifest 缺失场景)。
139
+ # 注意:development_pods 存的是 podspec 文件路径(如 /path/YYText.podspec.json)而非源码目录,
140
+ # 不能直接遍历它;统一走 sandbox.pod_dir —— 开发 pod 内部取 dirname、普通 pod 取 Pods/<名>
141
+ names = []
142
+ manifest = @sandbox.respond_to?(:manifest) ? @sandbox.manifest : nil
143
+ names.concat(manifest.pod_names) if manifest
144
+ names.concat(@sandbox.development_pods.keys)
145
+ names.uniq.each do |name|
146
+ dir = @sandbox.pod_dir(name)
147
+ roots[name] = dir.to_s if dir&.directory?
148
+ end
149
+
150
+ roots
151
+ end
152
+
153
+ # @return [Array<String>] 实际被改动文件的相对路径
154
+ def apply_rule(rule, pod_name, root)
155
+ files =
156
+ if rule.files
157
+ rule.files.map { |rel| File.join(root, rel) }.select { |f| File.file?(f) }
158
+ else
159
+ source_files(root)
160
+ end
161
+
162
+ files.filter_map do |file|
163
+ relative = file.sub("#{root}/", '')
164
+ relative if replace_in_file(file, rule)
165
+ end
166
+ end
167
+
168
+ # 遍历组件根下全部源码文件,剪枝 .git 与 .framework/.xcframework 等产物目录
169
+ def source_files(root)
170
+ result = []
171
+ Find.find(root) do |path|
172
+ base = File.basename(path)
173
+ if File.directory?(path)
174
+ # .framework/.xcframework 是目录名后缀(如 BinKit.framework),.git 为精确目录名
175
+ Find.prune if base == '.git' || base.end_with?('.framework', '.xcframework')
176
+ elsif File.file?(path) && SOURCE_SUFFIX.include?(File.extname(path).downcase)
177
+ result << path
178
+ end
179
+ end
180
+ result
181
+ rescue Errno::ENOENT
182
+ []
183
+ end
184
+
185
+ # @return [Boolean] 文件是否实际发生了改动
186
+ def replace_in_file(file, rule)
187
+ # 二进制方式读写:源码文件不一定是 UTF-8,按字节处理避免转码崩溃
188
+ content = File.binread(file)
189
+ updated =
190
+ if rule.pattern.is_a?(Regexp)
191
+ content.gsub(rule.pattern, rule.replacement)
192
+ else
193
+ # String 模式:替换内容原样使用,避免反斜杠被当成反向引用
194
+ content.gsub(rule.pattern) { rule.replacement }
195
+ end
196
+ return false if updated == content
197
+
198
+ # 三方库源码文件可能只读(历史上有此情况),先确保可写
199
+ FileUtils.chmod('u+w', file)
200
+ File.binwrite(file, updated)
201
+ true
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module MyConstants
3
+
4
+ # 本地依赖覆盖文件(全局唯一,不提交git)
5
+ LOCAL_DEPENDENCIES_YAML = 'LocalDependencies.yaml'
6
+ # 每个target的依赖文件后缀: <TargetName>Dependencies.yaml
7
+ DEPENDENCIES_YAML_SUFFIX = 'Dependencies.yaml'
8
+ # 打包进bundle的依赖清单后缀(仅Debug): <TargetName>Dependencies.json
9
+ DEPENDENCIES_JSON_SUFFIX = 'Dependencies.json'
10
+ PODFILE_LOCK = 'Podfile.lock'
11
+ PODFILE = 'Podfile'
12
+
13
+ LIBLIBARYS = %w[time removefile AppleTextureEncoder standards AvailabilityMacros semaphore SystemHealthClient dns_sd ftw checkint pwd bitstring utime err CommonCrypto inttypes resolv c++ _ctermid stdlib sysdir unwind fts vis AvailabilityVersions membership libxml2 ulimit _stdio ntsid net notify_keys CMakeLists.txt AssertMacros float aio libxml ConditionalMacros locale fmtmsg unicode _wctype cpio langinfo xlocale limits Endian unwind_itanium _types db bank _ctype unistd stddef dispatch wctype fcntl mach_debug Availability stringlist tar readpassphrase _locale ttyent bzlib cache signal dirent mpool libDER unwind_arm_ehabi spawn crt_externs simd regex AvailabilityInternal arpa syslog bsm netinet libunwind libgen corpses rpcsvc monetary compression expat_external setjmp tgmath notify getopt _regex dlfcn strings TargetConditionals fnmatch pthread xlocale execinfo networkext printf architecture alloca sys iconv fenv expat grp secure ctype fstab pthread_spis machine SystemHealthManager voucher wordexp zlib nl_types paths AppleEXR wchar sqlite3 util gethostuuid sched sqlite3ext iso646 Block netdb pthread math memory asl __wctype mach errno device termios copyfile os xattr_flags poll _xlocale stdio mach-o malloc string_x86 arm Spatial __libunwind_config nameser dns _types rpc netinet6 pthread_impl objc MacTypes libkern uuid search assert AppleArchive cache_callbacks dns_util glob ifaddrs utmpx sandbox arm64 stdint complex runetype zconf ucontext sysexits string ndbm]
14
+ FRAMEWORKS = %w[MetricKit Network GameKit AddressBookUI CreateML Speech SafariServices ProximityReader Metal AppClip QuartzCore CoreGraphics StoreKit GSS CoreML WatchConnectivity UserNotificationsUI ARKit CoreLocationUI MetalPerformanceShaders AutomaticAssessmentConfiguration ExternalAccessory MediaPlayer LinkPresentation IdentityLookupUI MediaToolbox UIKit MessageUI MetalFX CoreNFC HomeKit CryptoKit ScreenTime NewsstandKit MLCompute NaturalLanguage CoreVideo MetalKit OSLog PDFKit CoreText UniformTypeIdentifiers IOKit ManagedSettings AVFoundation Accelerate DeviceCheck ImageIO BackgroundTasks FamilyControls ClockKit CoreBluetooth ThreadNetwork SensorKit Security Contacts CarPlay ExtensionFoundation RealityKit PhotosUI RoomPlan ReplayKit IOSurface SpriteKit MobileCoreServices IntentsUI QuickLookThumbnailing CoreMedia BusinessChat Intents ColorSync VideoSubscriberAccount PencilKit ManagedSettingsUI CoreServices HealthKit MultipeerConnectivity BackgroundAssets WebKit NotificationCenter SystemConfiguration GameController CoreTelephony ActivityKit AVFAudio AssetsLibrary AudioUnit FileProvider DeviceActivity Social IdentityLookup Matter CoreImage CoreAudio MusicKit DeviceDiscoveryExtension MediaSetup AutomatedDeviceEnrollment CoreAudioKit ExposureNotification ClassKit VisionKit SwiftUI Combine ModelIO PHASE SharedWithYou OpenGLES MetalPerformanceShadersGraph CreateMLComponents Accounts FileProviderUI ShazamKit CarKey RealityFoundation QuickLook AppIntents AuthenticationServices WeatherKit DataDetection AudioToolbox NearbyInteraction ContactsUI CoreSpotlight LocalAuthentication MatterSupport Foundation AdSupport LocalAuthenticationEmbeddedUI Accessibility HealthKitUI PushKit Vision CoreAudioTypes NetworkExtension ExtensionKit OpenAL EventKit MediaAccessibility Charts iAd MapKit AppTrackingTransparency CoreHaptics CallKit CoreFoundation TabularData CoreMotion AVRouting WidgetKit AVKit AddressBook SoundAnalysis GroupActivities CoreLocation CloudKit SharedWithYouCore CFNetwork Photos JavaScriptCore GameplayKit PushToTalk CoreMIDI Twitter PassKit ImageCaptureCore CoreData GLKit AdServices Messages CryptoTokenKit VideoToolbox SafetyKit UserNotifications DeveloperToolsSupport CoreTransferable EventKitUI SceneKit]
15
+
16
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'my_constants'
2
+ require 'yaml'
3
+
4
+ module PodfileLock
5
+ class PodfileLockManager
6
+ # 读取podfile.lock
7
+ def self.podfile_lock_content
8
+ podfile_lock = MyConstants::PODFILE_LOCK
9
+
10
+ unless File.exist?(podfile_lock)
11
+ return {}
12
+ end
13
+
14
+ YAML.load_file(podfile_lock)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,201 @@
1
+ require 'json'
2
+ require_relative '../check/xcodeproj'
3
+ require_relative '../my_constants'
4
+ require_relative '../yaml/yaml_dependencies'
5
+
6
+ module Project
7
+ class ProjectManager
8
+
9
+ # 生成Debug包内的依赖清单json,并注入到对应target的resources脚本中(仅Debug)
10
+ # @param hook_context post_install钩子上下文:
11
+ # 插件hook拿到的是Pod::PostInstallHooksContext(umbrella_targets),
12
+ # Podfile里直接拿到的是Pod::Installer(aggregate_targets),两种都兼容
13
+ def self.dependency_manifest(hook_context)
14
+ CocoaPods::Yaml.log.step("依赖清单json生成(仅Debug入包)")
15
+
16
+ yaml_dep = Pod::YamlDependencies.new
17
+ records = yaml_dep.build_dependency_records
18
+
19
+ # 各target实际安装的组件根名(TRIM_UNUSED_POD裁剪后为真实子集)
20
+ installed_map = extract_installed_map(hook_context)
21
+ if installed_map.empty?
22
+ CocoaPods::Yaml.log.warn("未获取到target实际安装信息,跳过依赖清单生成")
23
+ return
24
+ end
25
+
26
+ records.each do |target_name, pod_records|
27
+ support_dir = "#{Dir.pwd}/Pods/Target Support Files/Pods-#{target_name}"
28
+ next unless File.directory?(support_dir)
29
+
30
+ installed = installed_map[target_name]
31
+ # 聚合target不存在(该target没有任何pod)时不产出清单
32
+ next if installed.nil?
33
+
34
+ # 按组件根名去重(子组件只保留一条),并过滤掉未实际安装的组件
35
+ merged_records = []
36
+ did_merge = {}
37
+ pod_records.each do |record|
38
+ root_name = record['pod'].to_s.split('/').first
39
+ next unless installed.include?(root_name)
40
+ next if did_merge[root_name]
41
+ did_merge[root_name] = true
42
+ merged_records << record
43
+ end
44
+ merged_records.sort_by! { |record| record['pod'].to_s.downcase }
45
+
46
+ json_name = yaml_dep.target_json_name(target_name)
47
+ manifest = {
48
+ 'target' => target_name,
49
+ 'cocoapods-yaml' => CocoaPods::Yaml::VERSION,
50
+ 'pods' => merged_records,
51
+ }
52
+ json_path = File.join(support_dir, json_name)
53
+ File.open(json_path, 'w') { |file| file.write(JSON.pretty_generate(manifest)) }
54
+
55
+ resource_shell_path = File.join(support_dir, "Pods-#{target_name}-resources.sh")
56
+ inject_manifest_resource(resource_shell_path, target_name, json_name)
57
+ end
58
+ end
59
+
60
+ # 从两种钩子上下文中提取 target名 => 实际安装组件根名列表
61
+ # Pod::PostInstallHooksContext(插件hook)用umbrella_targets;
62
+ # Pod::Installer(Podfile内post_install)用aggregate_targets
63
+ def self.extract_installed_map(hook_context)
64
+ installed_map = {}
65
+ return installed_map unless hook_context
66
+
67
+ if hook_context.respond_to?(:umbrella_targets)
68
+ hook_context.umbrella_targets.each do |umbrella|
69
+ label = umbrella.cocoapods_target_label.to_s
70
+ target_name = label.sub(/^Pods-/, '')
71
+ installed_map[target_name] = umbrella.specs.map { |spec| spec.root.name }.uniq
72
+ end
73
+ elsif hook_context.respond_to?(:aggregate_targets)
74
+ hook_context.aggregate_targets.each do |aggregate_target|
75
+ target_name = aggregate_target.name.sub(/^Pods-/, '')
76
+ installed_map[target_name] = aggregate_target.specs.map { |spec| spec.root.name }.uniq
77
+ end
78
+ end
79
+ installed_map
80
+ end
81
+
82
+ # 仅往Debug块注入清单资源,Release包不带任何依赖清单
83
+ def self.inject_manifest_resource(file_path, target_name, json_name)
84
+ return unless File.exist?(file_path)
85
+
86
+ content = IO.read(file_path)
87
+ return if content.include?(json_name)
88
+
89
+ resource_line = " install_resource \"${PODS_ROOT}/Target Support Files/Pods-#{target_name}/#{json_name}\"\n"
90
+ debug_marker = 'if [[ "$CONFIGURATION" == "Debug" ]]; then'
91
+
92
+ lines = content.lines
93
+ inserted = false
94
+ lines.each_with_index do |line, index|
95
+ next unless line.include?(debug_marker)
96
+ lines.insert(index + 1, resource_line)
97
+ inserted = true
98
+ break
99
+ end
100
+
101
+ unless inserted
102
+ CocoaPods::Yaml.log.warn("未找到Debug配置块,#{json_name}未注入(#{File.basename(file_path)})")
103
+ return
104
+ end
105
+
106
+ File.open(file_path, 'w') { |file| file.write(lines.join) }
107
+ end
108
+
109
+
110
+ # 将yaml文件加入Pods工程引用(仅为Xcode内可见可编辑),并把本地覆盖文件加入git忽略
111
+ def self.yaml_reference
112
+ CocoaPods::Yaml.log.step("yaml文件引用添加")
113
+ self.add_yaml
114
+ self.add_local_yaml_ignore
115
+ end
116
+
117
+ def self.add_yaml
118
+ yaml_dep = Pod::YamlDependencies.new
119
+ project_manager = XCProject::XcodeprojManager.share_manager
120
+ pods_project = project_manager.get_pods_project
121
+ return unless pods_project
122
+
123
+ yaml_files = dependency_yaml_files(yaml_dep, project_manager)
124
+
125
+ did_add = []
126
+ pods_project.main_group.children.each {|child|
127
+ did_add << child.display_name
128
+ }
129
+
130
+ yaml_files.each do |yaml_file|
131
+ unless did_add.include?(yaml_file)
132
+ path = "#{Dir.pwd}/#{yaml_file}"
133
+ pods_project.new_file(path)
134
+ end
135
+ end
136
+
137
+ pods_project.sort(:groups_position => :below)
138
+
139
+ # 依赖相关文件固定排在最前: Podfile、LocalDependencies.yaml、各target依赖文件(按工程target顺序)
140
+ preferred_names = ['Podfile'] + yaml_files
141
+ reorder_preferred_files(pods_project, preferred_names)
142
+
143
+ pods_project.save
144
+ end
145
+
146
+ # 将指定名称的文件引用移动到main_group最前面(保持preferred_names给定的顺序),其余文件顺序不变
147
+ def self.reorder_preferred_files(pods_project, preferred_names)
148
+ group = pods_project.main_group
149
+ children = group.children
150
+ preferred = preferred_names.map { |name| children.find { |child| child.display_name == name } }.compact
151
+ return if preferred.empty?
152
+ group.children.replace(preferred + (children - preferred))
153
+ end
154
+
155
+ # 需要在Pods工程中可见的yaml文件: 本地覆盖文件 + 各target依赖文件
156
+ def self.dependency_yaml_files(yaml_dep, project_manager)
157
+ files = []
158
+ local_name = yaml_dep.local_yaml_name
159
+ files << local_name if File.exist?(local_name)
160
+
161
+ project_manager.get_app_targets.each do |target|
162
+ next unless yaml_dep.integratable_target?(target)
163
+ yaml_name = yaml_dep.target_yaml_name(target.name)
164
+ files << yaml_name if File.exist?(yaml_name)
165
+ end
166
+ files
167
+ end
168
+
169
+ # 把本地覆盖文件添加到忽略文件中
170
+ def self.add_local_yaml_ignore
171
+
172
+ gitignore = ".gitignore"
173
+ local_yaml = MyConstants::LOCAL_DEPENDENCIES_YAML
174
+
175
+ contain = false
176
+ gitignore_path = "#{Dir.pwd}/#{gitignore}"
177
+ if File.exist?(gitignore_path)
178
+ safe_readlines(gitignore_path).each do |line|
179
+ # include?兼容已有条目带/前缀、子目录路径、行尾注释等不同形态,避免重复追加
180
+ contain = true if line.include?(local_yaml)
181
+ break if contain
182
+ end
183
+ end
184
+
185
+ unless contain
186
+ File.open(gitignore_path, 'a+') { |file|
187
+ file.write("\n#{local_yaml}\n")
188
+ }
189
+ end
190
+ end
191
+
192
+ # 容错读取文件行:历史工程的.gitignore可能是CRLF行尾或GBK等非UTF-8编码。
193
+ # 直接给IO.readlines传encoding: 'utf-8'在内外编码相同时不会转码,非法字节仍会透传,
194
+ # 先按二进制读取再显式做一次同编码转换替换非法字节,避免后续处理抛invalid byte sequence
195
+ def self.safe_readlines(file_path)
196
+ content = File.read(file_path, mode: 'rb')
197
+ content.encode!('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: '?')
198
+ content.lines
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../my_constants'
2
+ require_relative '../yaml/yaml_dependencies'
3
+
4
+ module Unused
5
+ class UnusedManager
6
+
7
+ # 打印未被引用的依赖库
8
+ def self.check_unused
9
+
10
+ yaml_dep = Pod::YamlDependencies.new
11
+ log = CocoaPods::Yaml.log
12
+
13
+ if yaml_dep.trim_unused_pod?
14
+ msg = "💚 当前已开启未使用依赖裁剪,请关注上方日志 '下载依赖...'部分内容。"
15
+ msg += "\n #{MyConstants::LOCAL_DEPENDENCIES_YAML}文件内的依赖库不会被裁剪。如需关闭,有2种方式任选其一:"
16
+ msg += "\n 1. 在#{MyConstants::LOCAL_DEPENDENCIES_YAML}中把最外层 TRIM_UNUSED_POD 设置为false,然后重新pod install"
17
+ msg += "\n 2. 执行 pod install/update --full 可单次关闭"
18
+ log.info(msg, :green)
19
+ else
20
+ msg = "💛 检测到你未开启未使用依赖裁剪。开发模式下可开启,移除未被引用的依赖库以加速编译,有2种方式任选其一:"
21
+ msg += "\n 1. 在#{MyConstants::LOCAL_DEPENDENCIES_YAML}中把最外层 TRIM_UNUSED_POD 设置为true,然后重新pod install"
22
+ msg += "\n 2. 执行 pod install/update --trim 可单次开启"
23
+ log.info(msg, :yellow)
24
+ end
25
+ log.info('')
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'time'
5
+ require_relative '../yaml/yaml_dependencies'
6
+
7
+ module CocoaPods
8
+ module Yaml
9
+ # Podfile初始化: 生成接入cocoapods-yaml的模板Podfile
10
+ class PodfileSetup
11
+ TEMPLATE = <<~'PODFILE'
12
+ # frozen_string_literal: true
13
+ # cocoapods-yaml 插件会被CocoaPods自动加载,无需require;
14
+ # 若 pod 命令提示找不到 yaml_dependencies!,请执行: gem install cocoapods-yaml
15
+
16
+ platform :ios, '15.0'
17
+
18
+ yaml_dependencies! do |target_name|
19
+ # target_name: 当前集成的target名称(App/扩展),每个声明了依赖的target回调一次
20
+ # 可按target定制,例如:
21
+ # use_frameworks! :linkage => :static if target_name == 'ShareExtension'
22
+ use_frameworks!
23
+ end
24
+
25
+ post_install do |installer|
26
+ installer.pods_project.targets.each do |target|
27
+ target.build_configurations.each do |config|
28
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '$(inherited)'
29
+ end
30
+ end
31
+ end
32
+ PODFILE
33
+
34
+ # pod yaml init: 显式初始化
35
+ # 无Podfile直接生成;已有Podfile但未接入时先备份再替换
36
+ def self.run
37
+ setup = new
38
+ unless setup.xcodeproj?
39
+ CocoaPods::Yaml.log.error('当前目录未找到.xcodeproj工程文件,请在iOS工程根目录执行 pod yaml init')
40
+ exit 1
41
+ end
42
+
43
+ if File.exist?('Podfile')
44
+ if setup.integrated?
45
+ CocoaPods::Yaml.log.info('当前工程已接入cocoapods-yaml,无需初始化')
46
+ return
47
+ end
48
+ setup.backup_podfile
49
+ setup.write_template
50
+ else
51
+ setup.write_template
52
+ end
53
+
54
+ setup.generate_yamls
55
+ end
56
+
57
+ # pod install/update 时自动初始化: 仅限无Podfile的零风险场景
58
+ # 已有Podfile但未接入时只引导,不自动替换
59
+ def self.auto_on_install
60
+ setup = new
61
+ return unless setup.xcodeproj?
62
+
63
+ if File.exist?('Podfile')
64
+ unless setup.integrated?
65
+ CocoaPods::Yaml.log.warn('检测到Podfile未接入cocoapods-yaml,执行 pod yaml init 可备份后生成模板(不会自动替换现有Podfile)')
66
+ end
67
+ return
68
+ end
69
+
70
+ setup.write_template
71
+ CocoaPods::Yaml.log.detail('未找到Podfile,已自动生成接入cocoapods-yaml的模板Podfile,依赖yaml文件将在解析时生成')
72
+ end
73
+
74
+ # 当前目录是否存在xcodeproj
75
+ def xcodeproj?
76
+ Dir.glob('*.xcodeproj').any?
77
+ end
78
+
79
+ # Podfile是否已接入yaml_dependencies!
80
+ def integrated?
81
+ File.read('Podfile').include?('yaml_dependencies!')
82
+ rescue StandardError
83
+ false
84
+ end
85
+
86
+ def backup_podfile
87
+ backup_name = "Podfile.bak.#{Time.now.strftime('%Y%m%d%H%M%S')}"
88
+ FileUtils.cp('Podfile', backup_name)
89
+ CocoaPods::Yaml.log.detail("已备份原Podfile为: #{backup_name}")
90
+ end
91
+
92
+ def write_template
93
+ File.write('Podfile', TEMPLATE)
94
+ CocoaPods::Yaml.log.detail('已生成模板Podfile')
95
+ end
96
+
97
+ # 生成本地覆盖文件与各target依赖文件模板
98
+ def generate_yamls
99
+ yaml_dep = Pod::YamlDependencies.new
100
+ yaml_dep.init_main_targets
101
+ yaml_dep.generate_yaml
102
+ rescue StandardError => e
103
+ CocoaPods::Yaml.log.warn("依赖yaml模板生成失败(不影响已生成的Podfile): #{e.message}")
104
+ end
105
+ end
106
+ end
107
+ end