cocoapods-source-fix 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/README.md +162 -0
- data/config.example.yml +86 -0
- data/lib/cocoapods-source-fix/configuration.rb +39 -0
- data/lib/cocoapods-source-fix/logger.rb +20 -0
- data/lib/cocoapods-source-fix/rule.rb +72 -0
- data/lib/cocoapods-source-fix/runner.rb +150 -0
- data/lib/cocoapods-source-fix/version.rb +3 -0
- data/lib/cocoapods-source-fix.rb +70 -0
- data/lib/cocoapods_plugin.rb +6 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '08cd70f594d0ebf1ec6b40982fee5ec16e2caf54faf9335ffb953dcd54df929b'
|
|
4
|
+
data.tar.gz: 30347776d6e39f8575937f225754da7b7d7a37de3f86831738ecd1e9500d07bf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7917a8bec1021d0721de8a7ef524951054493496dbd5480b2b39006c1c0298e342945898404ae8d42f91dea94fa81f16f4d7b73457635e93895cbe93d6f147d9
|
|
7
|
+
data.tar.gz: 6ea0f0d4652d8c6224db28d2ff36fe9d7a54f3245fc00dc99ad849e22ffcfa68760cfd1b120f70f0dd44e43626afb38aa47cd2bd1ef47d8d87ac0d642a4c432f
|
data/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# cocoapods-source-fix
|
|
2
|
+
|
|
3
|
+
CocoaPods 插件:在 `pod install` 后自动对指定 Pod 的源码/头文件做**文本替换**,用于修复第三方库的编译问题(表达式修复、私有头引入修复、任意文本替换等)。
|
|
4
|
+
|
|
5
|
+
> 原名 `cocoapods-header-fix` 已废弃——插件能力不止「头文件修复」(如 YYText 是表达式修复),故更名为 `cocoapods-source-fix`(源码文本替换修复)。
|
|
6
|
+
|
|
7
|
+
## 特点
|
|
8
|
+
|
|
9
|
+
- **Podfile 零改动**:通过 monkey-patch `Pod::Installer#install!`(与 `cocoapods-local-override` 相同机制,绕过 HooksManager 白名单),gem 以 `cocoapods-*` 前缀安装后由 CocoaPods 自动加载即生效,**无需在 Podfile 写 `plugin` 指令**
|
|
10
|
+
- **配置独立**:所有替换规则写在一个独立 YAML 文件中(默认 `Podfile 同目录/cocoapods-source-fix.yml`)
|
|
11
|
+
- **6 种目标定位**:相对目录 / 绝对目录 / 相对文件 / 绝对文件 / Pod 模块 / 全局
|
|
12
|
+
- **幂等**:重复执行 `pod install` 不重复破坏文件(内容无变化则跳过写文件)
|
|
13
|
+
- **安全**:写文件前 `chmod 0644`;文件不存在 / glob 无匹配静默跳过
|
|
14
|
+
|
|
15
|
+
## 安装与集成
|
|
16
|
+
|
|
17
|
+
### 方式一:Gemfile(推荐,项目用 `bundle exec pod install` 时)
|
|
18
|
+
|
|
19
|
+
在项目的 Gemfile 中加:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'cocoapods-source-fix', :path => '/Volumes/macc/ZCZY56/Dispatch-dymic/Dispatch-workspace/cocoapods-source-fix'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
然后 `bundle install && bundle exec pod install`。
|
|
26
|
+
|
|
27
|
+
### 方式二:gem 全局安装(项目直接用 `pod install` 时)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd /Volumes/macc/ZCZY56/Dispatch-dymic/Dispatch-workspace/cocoapods-source-fix
|
|
31
|
+
gem build cocoapods-source-fix.gemspec
|
|
32
|
+
gem install cocoapods-source-fix-0.1.0.gem
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
之后 `pod install` 自动生效。
|
|
36
|
+
|
|
37
|
+
## 配置
|
|
38
|
+
|
|
39
|
+
创建 `cocoapods-source-fix.yml` 放在 **Podfile 同目录**(或通过环境变量 `COCOAPODS_SOURCE_FIX_CONFIG` 指定路径):
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
rules:
|
|
43
|
+
# Pod 模块:glob 相对该 Pod 目录
|
|
44
|
+
- name: YYText 表达式修复
|
|
45
|
+
pod: YYText
|
|
46
|
+
glob: 'YYText/Component/YYTextLayout.m'
|
|
47
|
+
pairs:
|
|
48
|
+
- from: 'fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next)'
|
|
49
|
+
to: '(fabs(left - point.y) < fabs(right - point.y)) == (right ? prev : next)'
|
|
50
|
+
- from: 'fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next)'
|
|
51
|
+
to: '(fabs(left - point.x) < fabs(right - point.x)) == (right ? prev : next)'
|
|
52
|
+
|
|
53
|
+
# 相对 Podfile 的目录
|
|
54
|
+
- dir: 'Pods/SomePod'
|
|
55
|
+
glob: '**/*.{h,m}'
|
|
56
|
+
from: 'OLD_TEXT'
|
|
57
|
+
to: 'NEW_TEXT'
|
|
58
|
+
|
|
59
|
+
# 绝对路径目录
|
|
60
|
+
- dir: '/Users/xxx/Source/Pods/SomePod'
|
|
61
|
+
glob: '**/*.m'
|
|
62
|
+
from: 'OLD_TEXT'
|
|
63
|
+
to: 'NEW_TEXT'
|
|
64
|
+
|
|
65
|
+
# 相对 Podfile 的文件
|
|
66
|
+
- file: 'Pods/SomePod/SomePod/Foo.m'
|
|
67
|
+
from: 'OLD_TEXT'
|
|
68
|
+
to: 'NEW_TEXT'
|
|
69
|
+
|
|
70
|
+
# 绝对文件
|
|
71
|
+
- file: '/Users/xxx/Source/Pods/SomePod/SomePod/Foo.m'
|
|
72
|
+
from: 'OLD_TEXT'
|
|
73
|
+
to: 'NEW_TEXT'
|
|
74
|
+
|
|
75
|
+
# 不指定目标:全局替换(glob 相对 Podfile 目录)
|
|
76
|
+
- glob: 'Pods/**/*.{h,m}'
|
|
77
|
+
from: 'OLD_TEXT'
|
|
78
|
+
to: 'NEW_TEXT'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
完整可运行示例见 [config.example.yml](config.example.yml)。
|
|
82
|
+
|
|
83
|
+
### 规则字段
|
|
84
|
+
|
|
85
|
+
| 字段 | 必填 | 说明 |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `name` | 否 | 日志标签,便于识别规则 |
|
|
88
|
+
| `pod` / `dir` / `file` | 六选一 | 目标定位;均不指定则为全局(glob 相对 Podfile 目录) |
|
|
89
|
+
| `glob` | 目录/全局必填 | 文件匹配模式;Pod 模块下相对 Pod 目录,目录/全局下相对目标目录 |
|
|
90
|
+
| `exclude` | 否 | 排除文件/目录,字符串或数组,glob 模式,基准与 `glob` 相同;`'xx/**'` 或 `'**'` 表示排除整棵目录树 |
|
|
91
|
+
| `from` / `to` | 至少一组 | 单组字面量替换(`regex: true` 时按正则) |
|
|
92
|
+
| `pairs` | 否 | 多组替换 `[{from, to, regex?}]` |
|
|
93
|
+
| `regex` | 否 | 默认 `false`(字面量替换,自动转义正则元字符);`true` 时按正则替换 |
|
|
94
|
+
| `enabled` | 否 | 默认 `true`,可临时关闭某条规则 |
|
|
95
|
+
|
|
96
|
+
`exclude` 使用示例:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
- name: LFPhoneInfo 头引入方式修复
|
|
100
|
+
pod: LFPhoneInfo
|
|
101
|
+
glob: '**/*.{h,m,mm,swift}'
|
|
102
|
+
from: '#import <LFPhoneDefine.h>'
|
|
103
|
+
to: '#import "LFPhoneDefine.h"'
|
|
104
|
+
exclude:
|
|
105
|
+
- 'LFPhoneInfo/LFPhoneInfo.h' # 排除单个文件
|
|
106
|
+
- 'LFPhoneInfo/Private/**' # 排除整棵目录树
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 日志输出
|
|
110
|
+
|
|
111
|
+
日志统一走 `Pod::UI.puts`,格式参照 `cocoapods-user-defined-build-types` 插件(`🛠️ [cocoapods-source-fix] ...` 蓝色)。**不修复也会输出日志**,并区分原因:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
🛠️ [cocoapods-source-fix] 使用配置文件:/path/to/cocoapods-source-fix.yml(3 条规则)
|
|
115
|
+
🛠️ [cocoapods-source-fix] patching source files...
|
|
116
|
+
🛠️ [cocoapods-source-fix] processing rule: YYText 表达式修复
|
|
117
|
+
🛠️ [cocoapods-source-fix] ✅ 已修复文件:/path/to/Pods/YYText/YYText/Component/YYTextLayout.m
|
|
118
|
+
🛠️ [cocoapods-source-fix] processing rule: AFNetworking 私有头修复
|
|
119
|
+
🛠️ [cocoapods-source-fix] ⏭️ 跳过(2 个文件内容已是最新,无需修复)
|
|
120
|
+
🛠️ [cocoapods-source-fix] processing rule: 某条无匹配的规则
|
|
121
|
+
🛠️ [cocoapods-source-fix] ⏭️ 跳过(无匹配文件)
|
|
122
|
+
🛠️ [cocoapods-source-fix] finished patching source files (1 fixed, 2 skipped)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
各场景日志:
|
|
126
|
+
|
|
127
|
+
| 场景 | 日志 |
|
|
128
|
+
|------|------|
|
|
129
|
+
| 修复了文件 | `✅ 已修复文件:<path>` |
|
|
130
|
+
| 匹配到文件但内容已是最新(幂等) | `⏭️ 跳过(N 个文件内容已是最新,无需修复)` |
|
|
131
|
+
| 无文件匹配 glob | `⏭️ 跳过(无匹配文件)` |
|
|
132
|
+
| 读取失败 | `⚠️ 读取失败:<path>` |
|
|
133
|
+
| 结束汇总 | `finished patching source files (N fixed, M skipped)` |
|
|
134
|
+
|
|
135
|
+
### 详细日志(verbose)
|
|
136
|
+
|
|
137
|
+
默认只输出规则级汇总;配置 `verbose: true`(或环境变量 `COCOAPODS_SOURCE_FIX_VERBOSE=1`)后逐文件输出:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
🛠️ [cocoapods-source-fix] ⏭️ 已是最新:/path/to/Pods/YYText/YYText/Component/YYTextLayout.m
|
|
141
|
+
🛠️ [cocoapods-source-fix] 🚫 已排除:/path/to/Pods/LFPhoneInfo/LFPhoneInfo/Private/Secret.h
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- 每个实际发生替换的文件:`✅ 已修复文件:<path>`
|
|
145
|
+
- 无匹配的规则:`⏭️ 跳过(无匹配)`
|
|
146
|
+
- 配置文件不存在时提示跳过,不中断安装
|
|
147
|
+
|
|
148
|
+
## 文档导航
|
|
149
|
+
|
|
150
|
+
| 文件 | 说明 |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `REQUIREMENTS.md` | 需求梳理(背景 / 方案 / 验收标准) |
|
|
153
|
+
| `lib/` | 插件实现 |
|
|
154
|
+
| `config.example.yml` | 配置示例(覆盖 6 种目标定位) |
|
|
155
|
+
| `cocoapods-source-fix.gemspec` | gem 定义 |
|
|
156
|
+
|
|
157
|
+
## 验证
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pod install # 首次:看到 ✅ 已修复文件
|
|
161
|
+
pod install # 再次:看到 ⏭️ 跳过(幂等验证)
|
|
162
|
+
```
|
data/config.example.yml
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# cocoapods-source-fix 配置文件示例
|
|
3
|
+
#
|
|
4
|
+
# 默认位置:Podfile 同目录下的 cocoapods-source-fix.yml
|
|
5
|
+
# 也可用环境变量指定:COCOAPODS_SOURCE_FIX_CONFIG=/path/to/config.yml pod install
|
|
6
|
+
#
|
|
7
|
+
# 目标定位(每条规则六选一,均不指定则为全局替换):
|
|
8
|
+
# pod: '模块名' → 定位 Pod 目录(installer.sandbox.pod_dir),glob 相对该目录
|
|
9
|
+
# dir: '路径' → 目录;绝对路径直接用,相对路径以 Podfile 目录为基准
|
|
10
|
+
# file: '路径' → 文件;绝对路径直接用,相对路径以 Podfile 目录为基准
|
|
11
|
+
# (无以上字段) → 全局;glob 以 Podfile 目录为基准(如 'Pods/**/*.{h,m}')
|
|
12
|
+
#
|
|
13
|
+
# 替换内容(from/to 与 pairs 至少提供一组;均可选 regex: true 走正则):
|
|
14
|
+
# from/to 单组字面量替换
|
|
15
|
+
# pairs 多组替换 [{from, to, regex?}]
|
|
16
|
+
#
|
|
17
|
+
# 排除(可选):exclude 支持字符串或数组,glob 模式,基准与 glob 相同
|
|
18
|
+
# (pod → Pod 目录;dir → 目标目录;全局 → Podfile 目录),命中文件不修复。
|
|
19
|
+
# 以 '/**' 结尾(或 '**')表示排除整个目录树。
|
|
20
|
+
#
|
|
21
|
+
# 详细日志(可选):verbose: true 时逐文件输出「已是最新」「已排除」,
|
|
22
|
+
# 默认 false(只输出规则级汇总)。也可用环境变量 COCOAPODS_SOURCE_FIX_VERBOSE=1 覆盖。
|
|
23
|
+
#
|
|
24
|
+
# 其他字段:name(日志标签,可选)、glob(文件匹配,目录/全局必填)、enabled(默认 true)
|
|
25
|
+
# ============================================================
|
|
26
|
+
|
|
27
|
+
rules:
|
|
28
|
+
# ---------- 示例 1:指定 Pod 模块 + 单文件多组替换(迁移自 YYText 表达式修复) ----------
|
|
29
|
+
- name: YYText 表达式修复
|
|
30
|
+
pod: YYText
|
|
31
|
+
glob: 'YYText/Component/YYTextLayout.m'
|
|
32
|
+
pairs:
|
|
33
|
+
- from: 'fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next)'
|
|
34
|
+
to: '(fabs(left - point.y) < fabs(right - point.y)) == (right ? prev : next)'
|
|
35
|
+
- from: 'fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next)'
|
|
36
|
+
to: '(fabs(left - point.x) < fabs(right - point.x)) == (right ? prev : next)'
|
|
37
|
+
|
|
38
|
+
# ---------- 示例 2:指定 Pod 模块 + 多文件 glob(迁移自 AFNetworking 私有头修复) ----------
|
|
39
|
+
- name: AFNetworking 私有头修复
|
|
40
|
+
pod: AFNetworking
|
|
41
|
+
glob: 'AFNetworking/AF{NetworkReachabilityManager,HTTPSessionManager}.m'
|
|
42
|
+
from: '#import <netinet6/in6.h>'
|
|
43
|
+
to: '#include <sys/socket.h>'
|
|
44
|
+
|
|
45
|
+
# ---------- 示例 3:指定 Pod 模块 + 目录遍历(迁移自 LFPhoneInfo 头引入修复) ----------
|
|
46
|
+
- name: LFPhoneInfo 头引入方式修复
|
|
47
|
+
pod: LFPhoneInfo
|
|
48
|
+
glob: '**/*.{h,m,mm,swift}'
|
|
49
|
+
from: '#import <LFPhoneDefine.h>'
|
|
50
|
+
to: '#import "LFPhoneDefine.h"'
|
|
51
|
+
# 排除示例:不修复 LFPhoneInfo.h 自身(exclude 也可传数组)
|
|
52
|
+
# exclude: 'LFPhoneInfo/LFPhoneInfo.h'
|
|
53
|
+
# 排除整棵目录树:
|
|
54
|
+
# exclude: 'LFPhoneInfo/Private/**'
|
|
55
|
+
|
|
56
|
+
# ---------- 示例 4:相对 Podfile 文件的目录 ----------
|
|
57
|
+
# - name: 相对目录示例
|
|
58
|
+
# dir: 'Pods/SomePod'
|
|
59
|
+
# glob: '**/*.{h,m}'
|
|
60
|
+
# from: 'OLD_TEXT'
|
|
61
|
+
# to: 'NEW_TEXT'
|
|
62
|
+
|
|
63
|
+
# ---------- 示例 5:绝对路径目录 ----------
|
|
64
|
+
# - name: 绝对目录示例
|
|
65
|
+
# dir: '/Users/xxx/Source/Pods/SomePod'
|
|
66
|
+
# glob: '**/*.m'
|
|
67
|
+
# from: 'OLD_TEXT'
|
|
68
|
+
# to: 'NEW_TEXT'
|
|
69
|
+
|
|
70
|
+
# ---------- 示例 6:相对 Podfile 文件的文件 ----------
|
|
71
|
+
# - name: 相对文件示例
|
|
72
|
+
# file: 'Pods/SomePod/SomePod/Foo.m'
|
|
73
|
+
# from: 'OLD_TEXT'
|
|
74
|
+
# to: 'NEW_TEXT'
|
|
75
|
+
|
|
76
|
+
# ---------- 示例 7:绝对文件 ----------
|
|
77
|
+
# - name: 绝对文件示例
|
|
78
|
+
# file: '/Users/xxx/Source/Pods/SomePod/SomePod/Foo.m'
|
|
79
|
+
# from: 'OLD_TEXT'
|
|
80
|
+
# to: 'NEW_TEXT'
|
|
81
|
+
|
|
82
|
+
# ---------- 示例 8:不指定目标(全局替换,glob 相对 Podfile 目录) ----------
|
|
83
|
+
# - name: 全局替换示例
|
|
84
|
+
# glob: 'Pods/**/*.{h,m}'
|
|
85
|
+
# from: 'OLD_TEXT'
|
|
86
|
+
# to: 'NEW_TEXT'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'cocoapods-source-fix/rule'
|
|
3
|
+
|
|
4
|
+
module CocoapodsSourceFix
|
|
5
|
+
# 读取独立配置文件(默认 <Podfile 目录>/cocoapods-source-fix.yml)
|
|
6
|
+
class Configuration
|
|
7
|
+
CONFIG_FILE_NAME = 'cocoapods-source-fix.yml'
|
|
8
|
+
ENV_KEY = 'COCOAPODS_SOURCE_FIX_CONFIG'
|
|
9
|
+
ENV_VERBOSE_KEY = 'COCOAPODS_SOURCE_FIX_VERBOSE'
|
|
10
|
+
|
|
11
|
+
attr_reader :path, :rules, :verbose
|
|
12
|
+
|
|
13
|
+
# 定位配置文件:环境变量优先,其次 Podfile 同目录默认文件名
|
|
14
|
+
def self.locate(podfile_dir)
|
|
15
|
+
env_path = ENV[ENV_KEY]
|
|
16
|
+
return Pathname.new(env_path) if env_path && File.file?(env_path)
|
|
17
|
+
|
|
18
|
+
default = File.join(podfile_dir, CONFIG_FILE_NAME)
|
|
19
|
+
File.file?(default) ? Pathname.new(default) : nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(path, podfile_dir:)
|
|
23
|
+
@path = path
|
|
24
|
+
@data = YAML.load_file(path) || {}
|
|
25
|
+
@rules = Array(@data['rules']).map { |raw| Rule.new(raw, base_dir: podfile_dir) }
|
|
26
|
+
@verbose = parse_verbose
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# verbose 优先级:环境变量 > 配置文件 > 默认 false
|
|
32
|
+
def parse_verbose
|
|
33
|
+
env = ENV[ENV_VERBOSE_KEY]
|
|
34
|
+
return %w[1 true yes on].include?(env.downcase) if env && !env.empty?
|
|
35
|
+
|
|
36
|
+
@data['verbose'] == true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module CocoapodsSourceFix
|
|
2
|
+
PLUGIN_NAME = 'cocoapods-source-fix'
|
|
3
|
+
|
|
4
|
+
# 统一日志出口,参照 cocoapods-user-defined-build-types 风格:
|
|
5
|
+
# Pod::UI.puts "🛠️ [cocoapods-user-defined-build-types] patching build types...".blue
|
|
6
|
+
module Logger
|
|
7
|
+
def self.info(msg)
|
|
8
|
+
line = "🛠️ [#{PLUGIN_NAME}] #{msg}"
|
|
9
|
+
if defined?(Pod) && defined?(Pod::UI)
|
|
10
|
+
begin
|
|
11
|
+
Pod::UI.puts line.blue
|
|
12
|
+
rescue StandardError
|
|
13
|
+
Pod::UI.puts line
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
Kernel.puts line
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
|
|
3
|
+
module CocoapodsSourceFix
|
|
4
|
+
# 单条替换规则
|
|
5
|
+
#
|
|
6
|
+
# 目标定位(互斥,均不指定则为全局):
|
|
7
|
+
# pod: 'YYText' → 定位到 installer.sandbox.pod_dir('YYText'),glob 相对该 Pod 目录
|
|
8
|
+
# dir: 'path' → 目录,路径绝对则直接使用,相对则相对 Podfile 目录
|
|
9
|
+
# file: 'path' → 文件,路径绝对则直接使用,相对则相对 Podfile 目录
|
|
10
|
+
# (无以上字段) → 全局,glob 相对 Podfile 目录(如 'Pods/**/*.{h,m}')
|
|
11
|
+
#
|
|
12
|
+
# 替换内容(至少一组):
|
|
13
|
+
# from/to 单组字面量替换;pairs 多组替换;均可选 regex: true 启用正则
|
|
14
|
+
#
|
|
15
|
+
# 排除(可选):
|
|
16
|
+
# exclude: '模式' 或 ['模式1', '模式2'],glob 模式,基准与 glob 相同
|
|
17
|
+
# (pod → Pod 目录;dir → 目标目录;全局 → Podfile 目录),命中文件不修复
|
|
18
|
+
class Rule
|
|
19
|
+
attr_reader :name, :target_type, :target, :glob, :replaces, :enabled, :excludes
|
|
20
|
+
|
|
21
|
+
def initialize(raw, base_dir:)
|
|
22
|
+
@raw = raw || {}
|
|
23
|
+
@base_dir = base_dir
|
|
24
|
+
@name = @raw['name']
|
|
25
|
+
@glob = @raw['glob']
|
|
26
|
+
@enabled = @raw.fetch('enabled', true)
|
|
27
|
+
@excludes = Array(@raw['exclude']).compact.map(&:to_s)
|
|
28
|
+
@replaces = parse_replaces
|
|
29
|
+
resolve_target
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def enabled?
|
|
33
|
+
@enabled
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def label
|
|
37
|
+
@name || "target=#{@target_type}#{@target ? "(#{@target})" : ''}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def parse_replaces
|
|
43
|
+
list = []
|
|
44
|
+
if @raw['from'] && @raw['to']
|
|
45
|
+
list << build_replace(@raw['from'], @raw['to'], @raw.fetch('regex', false))
|
|
46
|
+
end
|
|
47
|
+
Array(@raw['pairs']).each do |pair|
|
|
48
|
+
list << build_replace(pair['from'], pair['to'], pair.fetch('regex', false))
|
|
49
|
+
end
|
|
50
|
+
list
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_replace(from, to, regex)
|
|
54
|
+
{ from: from, to: to, regex: regex }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve_target
|
|
58
|
+
if @raw['pod']
|
|
59
|
+
@target_type = :pod
|
|
60
|
+
@target = @raw['pod']
|
|
61
|
+
elsif @raw['dir']
|
|
62
|
+
@target_type = :dir
|
|
63
|
+
@target = @raw['dir']
|
|
64
|
+
elsif @raw['file']
|
|
65
|
+
@target_type = :file
|
|
66
|
+
@target = @raw['file']
|
|
67
|
+
else
|
|
68
|
+
@target_type = :global
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'cocoapods-source-fix/logger'
|
|
3
|
+
|
|
4
|
+
module CocoapodsSourceFix
|
|
5
|
+
# 执行替换:收集目标文件 → 逐文件替换 → 幂等写回 → 日志输出
|
|
6
|
+
class Runner
|
|
7
|
+
def initialize(context, config, podfile_dir: nil)
|
|
8
|
+
@context = context
|
|
9
|
+
@config = config
|
|
10
|
+
@podfile_dir = podfile_dir || default_podfile_dir
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
Logger.info "patching source files..."
|
|
15
|
+
fixed_total = 0
|
|
16
|
+
skipped_total = 0
|
|
17
|
+
@config.rules.each do |rule|
|
|
18
|
+
next unless rule.enabled?
|
|
19
|
+
|
|
20
|
+
files = collect_files(rule)
|
|
21
|
+
fixed, skipped = process_rule(rule, files)
|
|
22
|
+
fixed_total += fixed
|
|
23
|
+
skipped_total += skipped
|
|
24
|
+
end
|
|
25
|
+
Logger.info "finished patching source files (#{fixed_total} fixed, #{skipped_total} skipped)"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# 按目标类型收集待处理文件(含 exclude 过滤)
|
|
31
|
+
def collect_files(rule)
|
|
32
|
+
base_dir, files = files_for_target(rule)
|
|
33
|
+
apply_excludes(files, rule, base_dir)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def files_for_target(rule)
|
|
37
|
+
case rule.target_type
|
|
38
|
+
when :pod
|
|
39
|
+
dir = @context.sandbox.pod_dir(rule.target).to_s
|
|
40
|
+
[dir, glob_files(dir, rule.glob || '**/*')]
|
|
41
|
+
when :dir
|
|
42
|
+
dir = resolve_path(rule.target)
|
|
43
|
+
[dir, glob_files(dir, rule.glob || '**/*')]
|
|
44
|
+
when :file
|
|
45
|
+
path = resolve_path(rule.target)
|
|
46
|
+
[File.dirname(path), File.file?(path) ? [path] : []]
|
|
47
|
+
when :global
|
|
48
|
+
[@podfile_dir, glob_files(@podfile_dir, rule.glob || '**/*')]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# exclude 为 glob 模式,基准与 glob 相同(相对规则目标目录)。
|
|
53
|
+
# 注意:模式以 /** 结尾时 fnmatch 只匹配一层,追加 /**/* 变体覆盖整棵目录树。
|
|
54
|
+
def apply_excludes(files, rule, base_dir)
|
|
55
|
+
return files if rule.excludes.empty?
|
|
56
|
+
|
|
57
|
+
patterns = rule.excludes.flat_map do |pattern|
|
|
58
|
+
if pattern == '**' || pattern.end_with?('/**')
|
|
59
|
+
[pattern, File.join(pattern, '*')]
|
|
60
|
+
else
|
|
61
|
+
[pattern]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
65
|
+
|
|
66
|
+
files.reject do |file|
|
|
67
|
+
rel = file.delete_prefix(base_dir + '/')
|
|
68
|
+
hit = patterns.any? { |p| File.fnmatch(p, rel, flags) }
|
|
69
|
+
Logger.info " 🚫 已排除:#{file}" if hit && @config.verbose
|
|
70
|
+
hit
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 兜底:优先全局 Pod::Config 中的 Podfile,其次 sandbox_root 上级,最后 Dir.pwd
|
|
75
|
+
def default_podfile_dir
|
|
76
|
+
if defined?(Pod::Config) && Pod::Config.instance.respond_to?(:podfile)
|
|
77
|
+
pf = Pod::Config.instance.podfile
|
|
78
|
+
if pf && pf.respond_to?(:defined_in_file) && pf.defined_in_file
|
|
79
|
+
return File.dirname(pf.defined_in_file.to_s)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
if @context.respond_to?(:sandbox_root) && @context.sandbox_root
|
|
83
|
+
return File.dirname(@context.sandbox_root)
|
|
84
|
+
end
|
|
85
|
+
Dir.pwd
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def glob_files(dir, glob)
|
|
89
|
+
return [] unless File.directory?(dir)
|
|
90
|
+
|
|
91
|
+
Dir.glob(File.join(dir, glob)).select { |f| File.file?(f) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 绝对路径直接使用;相对路径以 Podfile 目录为基准
|
|
95
|
+
def resolve_path(path)
|
|
96
|
+
p = Pathname.new(path)
|
|
97
|
+
p.absolute? ? path : File.expand_path(path, @podfile_dir)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 返回 [fixed, skipped]:fixed 为实际写入的文件数,skipped 为内容已是最新(无变化)的文件数
|
|
101
|
+
def process_rule(rule, files)
|
|
102
|
+
Logger.info " processing rule: #{rule.label}"
|
|
103
|
+
if files.empty?
|
|
104
|
+
Logger.info " ⏭️ 跳过(无匹配文件)"
|
|
105
|
+
return [0, 0]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
touched = 0
|
|
109
|
+
skipped = 0
|
|
110
|
+
files.each do |file|
|
|
111
|
+
begin
|
|
112
|
+
content = File.read(file)
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
Logger.info " ⚠️ 读取失败:#{file}(#{e.message})"
|
|
115
|
+
next
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
new_content = rule.replaces.reduce(content) { |acc, rep| apply_replace(acc, rep) }
|
|
119
|
+
if new_content == content # 幂等:无变化跳过写文件
|
|
120
|
+
skipped += 1
|
|
121
|
+
Logger.info " ⏭️ 已是最新:#{file}" if @config.verbose
|
|
122
|
+
next
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
FileUtils.chmod 0644, file
|
|
126
|
+
File.write(file, new_content)
|
|
127
|
+
Logger.info " ✅ 已修复文件:#{file}"
|
|
128
|
+
touched += 1
|
|
129
|
+
end
|
|
130
|
+
if touched.zero?
|
|
131
|
+
Logger.info " ⏭️ 跳过(#{skipped} 个文件内容已是最新,无需修复)"
|
|
132
|
+
elsif skipped.positive?
|
|
133
|
+
Logger.info " ✅ 修复完成(#{touched} 个文件;#{skipped} 个已是最新)"
|
|
134
|
+
end
|
|
135
|
+
[touched, skipped]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def apply_replace(content, rep)
|
|
139
|
+
# 必须显式构造 Regexp:Ruby 3.x 下 gsub(字符串) 不会把字符串当正则源码解析
|
|
140
|
+
pattern = if rep[:regex]
|
|
141
|
+
Regexp.new(rep[:from])
|
|
142
|
+
else
|
|
143
|
+
# 默认字面量替换,避免被当作正则元字符
|
|
144
|
+
Regexp.new(Regexp.escape(rep[:from]))
|
|
145
|
+
end
|
|
146
|
+
# block 形式传 replacement,避免 \1 等被解释为捕获组引用
|
|
147
|
+
content.gsub(pattern) { rep[:to] }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'ostruct'
|
|
2
|
+
require 'cocoapods-source-fix/version'
|
|
3
|
+
require 'cocoapods-source-fix/logger'
|
|
4
|
+
require 'cocoapods-source-fix/configuration'
|
|
5
|
+
require 'cocoapods-source-fix/runner'
|
|
6
|
+
|
|
7
|
+
module CocoapodsSourceFix
|
|
8
|
+
class << self
|
|
9
|
+
# install! 完成后执行源码替换(加载即生效,无需 Podfile 声明 plugin)
|
|
10
|
+
def run_post_install(installer)
|
|
11
|
+
podfile_dir = podfile_dir_from(installer)
|
|
12
|
+
config_path = Configuration.locate(podfile_dir)
|
|
13
|
+
unless config_path
|
|
14
|
+
Logger.info "未找到配置文件(#{Configuration::ENV_KEY} 或 #{Configuration::CONFIG_FILE_NAME}),跳过替换"
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
config = Configuration.new(config_path, podfile_dir: podfile_dir)
|
|
19
|
+
Logger.info "使用配置文件:#{config_path}(#{config.rules.size} 条规则)"
|
|
20
|
+
# Runner 依赖 sandbox 定位 pod 目录,从 installer 实例提取并包装成轻量 context
|
|
21
|
+
context = OpenStruct.new(
|
|
22
|
+
sandbox: installer.sandbox,
|
|
23
|
+
sandbox_root: installer.sandbox.root.to_s
|
|
24
|
+
)
|
|
25
|
+
Runner.new(context, config, podfile_dir: podfile_dir).run
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 安装 monkey-patch:拦截 Pod::Installer#install!,在其执行完成后运行替换。
|
|
29
|
+
# 与 cocoapods-local-override 相同的机制——绕过 HooksManager 的 whitelist
|
|
30
|
+
# (whitelist 只放行 Podfile 中 plugin 声明的 hook),因此无需修改 Podfile。
|
|
31
|
+
def apply_installer_hook!
|
|
32
|
+
return if @installer_hook_applied
|
|
33
|
+
@installer_hook_applied = true
|
|
34
|
+
|
|
35
|
+
Pod::Installer.class_eval do
|
|
36
|
+
unless method_defined?(:source_fix_original_install!)
|
|
37
|
+
alias_method :source_fix_original_install!, :install!
|
|
38
|
+
|
|
39
|
+
define_method :install! do |*args|
|
|
40
|
+
source_fix_original_install!(*args)
|
|
41
|
+
CocoapodsSourceFix.run_post_install(self)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def podfile_dir_from(installer)
|
|
50
|
+
# 1. 全局 Pod::Config 中已加载的 Podfile(最可靠,含 defined_in_file)
|
|
51
|
+
if defined?(Pod::Config) && Pod::Config.instance.respond_to?(:podfile)
|
|
52
|
+
pf = Pod::Config.instance.podfile
|
|
53
|
+
if pf && pf.respond_to?(:defined_in_file) && pf.defined_in_file
|
|
54
|
+
return File.dirname(pf.defined_in_file.to_s)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
# 2. 兜底:sandbox.root(默认 <Podfile 目录>/Pods)的上级目录
|
|
58
|
+
if installer.respond_to?(:sandbox) && installer.sandbox &&
|
|
59
|
+
installer.sandbox.respond_to?(:root) && installer.sandbox.root
|
|
60
|
+
return File.dirname(installer.sandbox.root.to_s)
|
|
61
|
+
end
|
|
62
|
+
# 3. 最终兜底:当前工作目录
|
|
63
|
+
Dir.pwd
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 说明:CocoaPods 启动时自动 require gem 内的 lib/cocoapods_plugin.rb(无需 Podfile
|
|
69
|
+
# 声明),此处直接 monkey-patch Pod::Installer#install!,加载即生效。
|
|
70
|
+
CocoapodsSourceFix.apply_installer_hook!
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cocoapods-source-fix
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ZCW
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: cocoapods
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.0.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 1.0.0
|
|
27
|
+
description: Automatically apply text replacements (expression fixes, private header
|
|
28
|
+
import fixes, etc.) to Pod sources after `pod install`, configured via a standalone
|
|
29
|
+
YAML file. No Podfile modification required.
|
|
30
|
+
email:
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- README.md
|
|
36
|
+
- config.example.yml
|
|
37
|
+
- lib/cocoapods-source-fix.rb
|
|
38
|
+
- lib/cocoapods-source-fix/configuration.rb
|
|
39
|
+
- lib/cocoapods-source-fix/logger.rb
|
|
40
|
+
- lib/cocoapods-source-fix/rule.rb
|
|
41
|
+
- lib/cocoapods-source-fix/runner.rb
|
|
42
|
+
- lib/cocoapods-source-fix/version.rb
|
|
43
|
+
- lib/cocoapods_plugin.rb
|
|
44
|
+
homepage: https://github.com/charleszhao888888/cocoapods-source-fix
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata: {}
|
|
48
|
+
post_install_message:
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '2.6'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 3.3.3
|
|
64
|
+
signing_key:
|
|
65
|
+
specification_version: 4
|
|
66
|
+
summary: CocoaPods plugin to apply text replacements to pod sources after pod install
|
|
67
|
+
test_files: []
|