pindo 5.2.4 → 5.3.7
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 +4 -4
- data/lib/pindo/base/aeshelper.rb +23 -2
- data/lib/pindo/base/pindocontext.rb +259 -0
- data/lib/pindo/client/pgyer_feishu_oauth_cli.rb +343 -80
- data/lib/pindo/client/pgyerclient.rb +30 -20
- data/lib/pindo/command/android/autobuild.rb +52 -22
- data/lib/pindo/command/android/build.rb +27 -16
- data/lib/pindo/command/android/debug.rb +25 -15
- data/lib/pindo/command/dev/debug.rb +2 -51
- data/lib/pindo/command/dev/feishu.rb +19 -2
- data/lib/pindo/command/ios/adhoc.rb +2 -1
- data/lib/pindo/command/ios/autobuild.rb +35 -8
- data/lib/pindo/command/ios/debug.rb +2 -132
- data/lib/pindo/command/lib/lint.rb +24 -1
- data/lib/pindo/command/setup.rb +24 -4
- data/lib/pindo/command/unity/apk.rb +15 -0
- data/lib/pindo/command/unity/ipa.rb +16 -0
- data/lib/pindo/command.rb +13 -2
- data/lib/pindo/module/android/android_build_config_helper.rb +425 -0
- data/lib/pindo/module/android/apk_helper.rb +23 -25
- data/lib/pindo/module/android/base_helper.rb +572 -0
- data/lib/pindo/module/android/build_helper.rb +8 -318
- data/lib/pindo/module/android/gp_compliance_helper.rb +668 -0
- data/lib/pindo/module/android/gradle_helper.rb +746 -3
- data/lib/pindo/module/appselect.rb +18 -5
- data/lib/pindo/module/build/buildhelper.rb +120 -29
- data/lib/pindo/module/build/unityhelper.rb +675 -18
- data/lib/pindo/module/build/versionhelper.rb +146 -0
- data/lib/pindo/module/cert/certhelper.rb +33 -2
- data/lib/pindo/module/cert/xcodecerthelper.rb +3 -1
- data/lib/pindo/module/pgyer/pgyerhelper.rb +114 -31
- data/lib/pindo/module/xcode/xcodebuildconfig.rb +232 -0
- data/lib/pindo/module/xcode/xcodebuildhelper.rb +0 -1
- data/lib/pindo/version.rb +356 -86
- data/lib/pindo.rb +72 -3
- metadata +5 -1
@@ -0,0 +1,668 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'nokogiri'
|
4
|
+
require_relative 'base_helper'
|
5
|
+
|
6
|
+
module Pindo
|
7
|
+
module GPComplianceHelper
|
8
|
+
include BaseAndroidHelper
|
9
|
+
|
10
|
+
# Google Play 合规检测结果
|
11
|
+
class ComplianceResult
|
12
|
+
attr_accessor :target_sdk_compliant, :target_sdk_version, :elf_alignment_compliant,
|
13
|
+
:unaligned_libs, :total_libs, :compliance_issues, :warnings,
|
14
|
+
:size_compliant, :aab_size_mb, :base_size_mb, :base_percent
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@target_sdk_compliant = false
|
18
|
+
@target_sdk_version = 0
|
19
|
+
@elf_alignment_compliant = false
|
20
|
+
@unaligned_libs = []
|
21
|
+
@total_libs = 0
|
22
|
+
@compliance_issues = []
|
23
|
+
@warnings = []
|
24
|
+
@size_compliant = false
|
25
|
+
@aab_size_mb = 0
|
26
|
+
@base_size_mb = 0
|
27
|
+
@base_percent = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def compliant?
|
31
|
+
@target_sdk_compliant && @elf_alignment_compliant && @size_compliant
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_issue(issue)
|
35
|
+
@compliance_issues << issue
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_warning(warning)
|
39
|
+
@warnings << warning
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# 检测 AAB 文件的 Google Play 合规性
|
44
|
+
def self.check_aab_compliance(aab_path)
|
45
|
+
result = ComplianceResult.new
|
46
|
+
|
47
|
+
unless File.exist?(aab_path)
|
48
|
+
result.add_issue("AAB 文件不存在: #{aab_path}")
|
49
|
+
return result
|
50
|
+
end
|
51
|
+
|
52
|
+
puts "\n\e[1m=== Google Play 合规检测 ===\e[0m"
|
53
|
+
puts "检测文件: #{File.basename(aab_path)}"
|
54
|
+
|
55
|
+
# 创建临时目录用于解压 AAB
|
56
|
+
temp_dir = nil
|
57
|
+
begin
|
58
|
+
temp_dir = Dir.mktmpdir("aab_compliance_check_")
|
59
|
+
|
60
|
+
# 检查 unzip 工具是否可用
|
61
|
+
unless tool_available?('unzip')
|
62
|
+
result.add_issue("unzip 工具不可用,无法解压 AAB 文件")
|
63
|
+
puts "请安装 unzip 工具: brew install unzip (macOS) 或 apt-get install unzip (Ubuntu)"
|
64
|
+
return result
|
65
|
+
end
|
66
|
+
|
67
|
+
# 解压 AAB 文件
|
68
|
+
unless system("unzip", "-q", aab_path, "-d", temp_dir)
|
69
|
+
result.add_issue("无法解压 AAB 文件")
|
70
|
+
return result
|
71
|
+
end
|
72
|
+
|
73
|
+
# 检测 AAB 包体积
|
74
|
+
check_aab_size_compliance(aab_path, result)
|
75
|
+
|
76
|
+
# 检测 Target SDK 版本(传递 AAB 路径)
|
77
|
+
check_target_sdk_compliance(temp_dir, result, aab_path)
|
78
|
+
|
79
|
+
# 检测 ELF 对齐
|
80
|
+
check_elf_alignment_compliance(temp_dir, result)
|
81
|
+
|
82
|
+
# 输出检测结果
|
83
|
+
print_compliance_summary(result)
|
84
|
+
|
85
|
+
ensure
|
86
|
+
# 清理临时目录
|
87
|
+
FileUtils.rm_rf(temp_dir) if temp_dir && File.directory?(temp_dir)
|
88
|
+
end
|
89
|
+
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# 检查工具是否可用
|
96
|
+
def self.tool_available?(tool_name)
|
97
|
+
case tool_name
|
98
|
+
when 'bundletool'
|
99
|
+
# 检查 bundletool 是否可用(可能是别名或直接命令)
|
100
|
+
# 使用 Open3 来正确检测命令是否可用
|
101
|
+
require 'open3'
|
102
|
+
begin
|
103
|
+
stdout, stderr, status = Open3.capture3('bundletool help')
|
104
|
+
status.success?
|
105
|
+
rescue
|
106
|
+
false
|
107
|
+
end
|
108
|
+
when 'aapt'
|
109
|
+
system("which aapt > /dev/null 2>&1") || system("aapt version > /dev/null 2>&1")
|
110
|
+
when 'aapt2'
|
111
|
+
system("which aapt2 > /dev/null 2>&1") || system("aapt2 version > /dev/null 2>&1")
|
112
|
+
when 'objdump'
|
113
|
+
system("which objdump > /dev/null 2>&1")
|
114
|
+
when 'readelf'
|
115
|
+
system("which readelf > /dev/null 2>&1")
|
116
|
+
when 'unzip'
|
117
|
+
system("which unzip > /dev/null 2>&1")
|
118
|
+
else
|
119
|
+
system("which #{tool_name} > /dev/null 2>&1")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# 检测 AAB 包体积合规性
|
124
|
+
def self.check_aab_size_compliance(aab_path, result)
|
125
|
+
puts "\n\e[1m--- AAB 包体积检测 ---\e[0m"
|
126
|
+
|
127
|
+
begin
|
128
|
+
# 获取 AAB 文件大小
|
129
|
+
aab_size = File.size(aab_path)
|
130
|
+
result.aab_size_mb = (aab_size.to_f / 1024 / 1024).round(2)
|
131
|
+
|
132
|
+
# 统计 base/ 文件夹压缩体积
|
133
|
+
base_size = 0
|
134
|
+
base_limit_bytes = 194615705 # 185MB
|
135
|
+
base_limit_mb = 185
|
136
|
+
|
137
|
+
unzip_out = `unzip -v "#{aab_path}"`
|
138
|
+
if unzip_out && !unzip_out.empty?
|
139
|
+
base_lines = unzip_out.lines.select { |l| l.include?(" base/") }
|
140
|
+
base_size = base_lines.map { |l| l.split[2].to_i }.sum
|
141
|
+
result.base_size_mb = (base_size.to_f / 1024 / 1024).round(2)
|
142
|
+
result.base_percent = aab_size > 0 ? ((base_size.to_f * 100) / aab_size).round(2) : 0
|
143
|
+
end
|
144
|
+
|
145
|
+
puts "AAB 包体积: #{result.aab_size_mb} MB"
|
146
|
+
puts "base 文件夹压缩体积: #{result.base_size_mb} MB (占比 #{result.base_percent}%)"
|
147
|
+
|
148
|
+
if base_size > base_limit_bytes
|
149
|
+
result.size_compliant = false
|
150
|
+
result.add_issue("base 文件夹已超出 Google Play 限制(#{base_limit_mb}MB),请优化资源或分包")
|
151
|
+
puts "\e[31m✗ base 文件夹已超出 Google Play 限制(#{base_limit_mb}MB),请优化资源或分包!\e[0m"
|
152
|
+
else
|
153
|
+
result.size_compliant = true
|
154
|
+
puts "\e[32m✓ AAB 包体积符合 Google Play 提交标准\e[0m"
|
155
|
+
end
|
156
|
+
|
157
|
+
rescue => e
|
158
|
+
result.add_issue("AAB 包体积检测失败: #{e.message}")
|
159
|
+
puts "✗ AAB 包体积检测失败: #{e.message}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# 检测 Target SDK 版本合规性
|
164
|
+
def self.check_target_sdk_compliance(temp_dir, result, aab_path = nil)
|
165
|
+
puts "\n\e[1m--- Target SDK 版本检测 ---\e[0m"
|
166
|
+
|
167
|
+
target_sdk = 0
|
168
|
+
|
169
|
+
# 方法1: 使用 bundletool dump manifest(首要方法)
|
170
|
+
if aab_path && File.exist?(aab_path)
|
171
|
+
# puts "使用 bundletool 解析 AAB 文件: #{File.basename(aab_path)}"
|
172
|
+
target_sdk = extract_target_sdk_with_bundletool(aab_path)
|
173
|
+
end
|
174
|
+
|
175
|
+
# 方法2: 如果 bundletool 失败,使用二进制 XML 解析(备用方法)
|
176
|
+
if target_sdk == 0
|
177
|
+
puts "bundletool 解析失败,尝试二进制 XML 解析"
|
178
|
+
manifest_path = find_android_manifest(temp_dir)
|
179
|
+
if manifest_path
|
180
|
+
puts "找到 AndroidManifest.xml: #{manifest_path}"
|
181
|
+
target_sdk = extract_target_sdk_from_manifest(manifest_path)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
result.target_sdk_version = target_sdk
|
186
|
+
|
187
|
+
if target_sdk >= 35
|
188
|
+
result.target_sdk_compliant = true
|
189
|
+
puts "\e[32m✓ Target SDK: #{target_sdk} (符合 Android 15 要求)\e[0m"
|
190
|
+
else
|
191
|
+
result.target_sdk_compliant = false
|
192
|
+
if target_sdk == 0
|
193
|
+
result.add_issue("无法检测到 Target SDK 版本,请检查 AAB 文件结构")
|
194
|
+
else
|
195
|
+
result.add_issue("Target SDK #{target_sdk} 不符合要求,需要至少 Target SDK 35 (Android 15)")
|
196
|
+
end
|
197
|
+
puts "\e[31m✗ Target SDK: #{target_sdk} (需要至少 35)\e[0m"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
# 使用 bundletool 提取 Target SDK 版本
|
203
|
+
def self.extract_target_sdk_with_bundletool(aab_path)
|
204
|
+
begin
|
205
|
+
# 获取 Pindo 自带的 bundletool.jar 路径
|
206
|
+
bundletool_jar = get_pindo_bundletool_path
|
207
|
+
unless bundletool_jar && File.exist?(bundletool_jar)
|
208
|
+
puts "Pindo 自带的 bundletool.jar 不可用,尝试使用系统 bundletool 命令"
|
209
|
+
return extract_target_sdk_with_system_bundletool(aab_path)
|
210
|
+
end
|
211
|
+
|
212
|
+
# 使用 Pindo 自带的 bundletool.jar
|
213
|
+
# 确保使用正确的 Java 版本 (Java 11+)
|
214
|
+
java_cmd = get_java_command_for_bundletool
|
215
|
+
bundletool_cmd = "#{java_cmd} -jar \"#{bundletool_jar}\" dump manifest --bundle=\"#{aab_path}\" | grep \"targetSdkVersion\""
|
216
|
+
output = `#{bundletool_cmd} 2>/dev/null`
|
217
|
+
|
218
|
+
if output && !output.empty?
|
219
|
+
# puts "bundletool 输出: #{output.strip}"
|
220
|
+
|
221
|
+
# 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
|
222
|
+
if output =~ /android:targetSdkVersion="(\d+)"/
|
223
|
+
target_sdk = $1.to_i
|
224
|
+
# puts "从 bundletool 中找到 targetSdkVersion: #{target_sdk}"
|
225
|
+
return target_sdk
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# puts "bundletool 未找到 targetSdkVersion"
|
230
|
+
return 0
|
231
|
+
rescue => e
|
232
|
+
puts "bundletool 解析失败: #{e.message}"
|
233
|
+
return 0
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# 使用系统 bundletool 命令(备用方法)
|
238
|
+
def self.extract_target_sdk_with_system_bundletool(aab_path)
|
239
|
+
begin
|
240
|
+
# 检查系统 bundletool 是否可用
|
241
|
+
unless tool_available?('bundletool')
|
242
|
+
puts "系统 bundletool 也不可用,请确保已安装 bundletool"
|
243
|
+
puts "安装方法: 下载 bundletool.jar 并添加到 PATH 或创建别名"
|
244
|
+
return 0
|
245
|
+
end
|
246
|
+
|
247
|
+
# 使用系统 bundletool 命令
|
248
|
+
bundletool_cmd = "bundletool dump manifest --bundle=\"#{aab_path}\" | grep \"targetSdkVersion\""
|
249
|
+
output = `#{bundletool_cmd} 2>/dev/null`
|
250
|
+
|
251
|
+
if output && !output.empty?
|
252
|
+
# puts "bundletool 输出: #{output.strip}"
|
253
|
+
|
254
|
+
# 解析输出格式: <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34"/>
|
255
|
+
if output =~ /android:targetSdkVersion="(\d+)"/
|
256
|
+
target_sdk = $1.to_i
|
257
|
+
# puts "从系统 bundletool 中找到 targetSdkVersion: #{target_sdk}"
|
258
|
+
return target_sdk
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# puts "系统 bundletool 未找到 targetSdkVersion"
|
263
|
+
return 0
|
264
|
+
rescue => e
|
265
|
+
puts "系统 bundletool 解析失败: #{e.message}"
|
266
|
+
return 0
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# 获取 Pindo 自带的 bundletool.jar 路径
|
271
|
+
def self.get_pindo_bundletool_path
|
272
|
+
begin
|
273
|
+
# 使用与 apk_helper.rb 相同的方法获取工具路径
|
274
|
+
pindo_dir = File.expand_path(ENV['PINDO_DIR'] || '~/.pindo')
|
275
|
+
pindo_common_configdir = File.join(pindo_dir, "pindo_common_config")
|
276
|
+
tools_dir = File.join(pindo_common_configdir, 'android_tools')
|
277
|
+
bundletool_jar = File.join(tools_dir, 'bundletool.jar')
|
278
|
+
|
279
|
+
File.exist?(bundletool_jar) ? bundletool_jar : nil
|
280
|
+
rescue
|
281
|
+
nil
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# 获取用于 bundletool 的 Java 命令
|
286
|
+
def self.get_java_command_for_bundletool
|
287
|
+
# 创建一个临时对象来调用 BaseAndroidHelper 的方法
|
288
|
+
helper = Object.new
|
289
|
+
helper.extend(BaseAndroidHelper)
|
290
|
+
helper.find_java_command
|
291
|
+
end
|
292
|
+
|
293
|
+
# 查找 AndroidManifest.xml 文件(备用方法)
|
294
|
+
def self.find_android_manifest(temp_dir)
|
295
|
+
# 查找可能的 AndroidManifest.xml 路径
|
296
|
+
possible_paths = [
|
297
|
+
# AAB 解压后的标准路径
|
298
|
+
File.join(temp_dir, "base", "manifest", "AndroidManifest.xml"),
|
299
|
+
File.join(temp_dir, "manifest", "AndroidManifest.xml"),
|
300
|
+
# 其他可能的路径
|
301
|
+
File.join(temp_dir, "AndroidManifest.xml"),
|
302
|
+
# 递归查找
|
303
|
+
Dir.glob(File.join(temp_dir, "**", "AndroidManifest.xml")).first
|
304
|
+
]
|
305
|
+
|
306
|
+
possible_paths.each do |path|
|
307
|
+
return path if path && File.exist?(path)
|
308
|
+
end
|
309
|
+
|
310
|
+
nil
|
311
|
+
end
|
312
|
+
|
313
|
+
# 从 AndroidManifest.xml 中提取 Target SDK 版本
|
314
|
+
def self.extract_target_sdk_from_manifest(manifest_path)
|
315
|
+
begin
|
316
|
+
# 读取文件内容(二进制模式)
|
317
|
+
content = File.binread(manifest_path)
|
318
|
+
|
319
|
+
# 检查是否为二进制 XML
|
320
|
+
if content.start_with?("\x03\x00\x08\x00") || content.include?("\x00")
|
321
|
+
puts "检测到二进制 XML 格式,使用二进制解析方法"
|
322
|
+
|
323
|
+
# 在二进制 XML 中搜索 targetSdkVersion 后的数字
|
324
|
+
# 模式: targetSdkVersion + 一些二进制数据 + 数字
|
325
|
+
if content =~ /targetSdkVersion[^\d]*(\d{1,2})/
|
326
|
+
# puts "从二进制 XML 中找到 targetSdkVersion: #{$1}"
|
327
|
+
return $1.to_i
|
328
|
+
end
|
329
|
+
|
330
|
+
# 尝试更宽泛的搜索模式
|
331
|
+
if content =~ /targetSdkVersion.*?(\d{1,2})/
|
332
|
+
# puts "从二进制 XML 中找到 targetSdkVersion (宽泛模式): #{$1}"
|
333
|
+
return $1.to_i
|
334
|
+
end
|
335
|
+
|
336
|
+
# puts "二进制 XML 中未找到 targetSdkVersion"
|
337
|
+
return 0
|
338
|
+
end
|
339
|
+
|
340
|
+
# 尝试使用 aapt 工具解析(适用于文本格式)
|
341
|
+
if tool_available?('aapt')
|
342
|
+
aapt_output = `aapt dump badging "#{manifest_path}" 2>/dev/null`
|
343
|
+
if aapt_output && !aapt_output.empty?
|
344
|
+
if aapt_output =~ /targetSdkVersion:'(\d+)'/
|
345
|
+
# puts "从 aapt 中找到 targetSdkVersion: #{$1}"
|
346
|
+
return $1.to_i
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# 尝试使用 aapt2 工具
|
352
|
+
if tool_available?('aapt2')
|
353
|
+
aapt2_output = `aapt2 dump badging "#{manifest_path}" 2>/dev/null`
|
354
|
+
if aapt2_output && !aapt2_output.empty?
|
355
|
+
if aapt2_output =~ /targetSdkVersion:'(\d+)'/
|
356
|
+
# puts "从 aapt2 中找到 targetSdkVersion: #{$1}"
|
357
|
+
return $1.to_i
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# 尝试使用 aapt dump xmltree
|
363
|
+
if tool_available?('aapt')
|
364
|
+
aapt_xml_output = `aapt dump xmltree "#{manifest_path}" 2>/dev/null`
|
365
|
+
if aapt_xml_output && !aapt_xml_output.empty?
|
366
|
+
if aapt_xml_output =~ /targetSdkVersion.*?(\d+)/
|
367
|
+
# puts "从 aapt xmltree 中找到 targetSdkVersion: #{$1}"
|
368
|
+
return $1.to_i
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# 解析文本 XML
|
374
|
+
doc = Nokogiri::XML(content)
|
375
|
+
|
376
|
+
# 处理命名空间
|
377
|
+
doc.remove_namespaces!
|
378
|
+
|
379
|
+
# 查找 uses-sdk 标签
|
380
|
+
uses_sdk = doc.at_xpath('//uses-sdk')
|
381
|
+
if uses_sdk
|
382
|
+
target_sdk = uses_sdk['targetSdkVersion']
|
383
|
+
if target_sdk
|
384
|
+
# puts "从 XML 中找到 targetSdkVersion: #{target_sdk}"
|
385
|
+
return target_sdk.to_i
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# 如果没找到 uses-sdk 标签,尝试查找其他可能的标签
|
390
|
+
target_sdk_attrs = doc.xpath('//@targetSdkVersion')
|
391
|
+
if !target_sdk_attrs.empty?
|
392
|
+
target_sdk = target_sdk_attrs.first.value
|
393
|
+
# puts "从 XML 属性中找到 targetSdkVersion: #{target_sdk}"
|
394
|
+
return target_sdk.to_i
|
395
|
+
end
|
396
|
+
|
397
|
+
# 尝试查找所有包含 targetSdkVersion 的属性
|
398
|
+
all_attrs = doc.xpath('//@*[contains(name(), "targetSdkVersion")]')
|
399
|
+
if !all_attrs.empty?
|
400
|
+
target_sdk = all_attrs.first.value
|
401
|
+
# puts "从 XML 中找到 targetSdkVersion 属性: #{target_sdk}"
|
402
|
+
return target_sdk.to_i
|
403
|
+
end
|
404
|
+
|
405
|
+
# puts "未找到 targetSdkVersion,返回默认值 0"
|
406
|
+
return 0
|
407
|
+
|
408
|
+
rescue => e
|
409
|
+
puts "解析 AndroidManifest.xml 失败: #{e.message}"
|
410
|
+
return 0
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# 检测 ELF 对齐合规性
|
415
|
+
def self.check_elf_alignment_compliance(temp_dir, result)
|
416
|
+
puts "\n\e[1m--- ELF 对齐检测 (16KB 页面大小) ---\e[0m"
|
417
|
+
|
418
|
+
# 查找所有 .so 文件
|
419
|
+
so_files = find_shared_libraries(temp_dir)
|
420
|
+
result.total_libs = so_files.length
|
421
|
+
|
422
|
+
if so_files.empty?
|
423
|
+
puts "未找到共享库文件"
|
424
|
+
result.elf_alignment_compliant = true
|
425
|
+
return
|
426
|
+
end
|
427
|
+
|
428
|
+
puts "找到 #{so_files.length} 个共享库文件"
|
429
|
+
|
430
|
+
unaligned_libs = []
|
431
|
+
|
432
|
+
so_files.each do |so_file|
|
433
|
+
alignment_status = check_elf_alignment(so_file)
|
434
|
+
|
435
|
+
if alignment_status[:aligned]
|
436
|
+
puts "\e[32m✓ #{File.basename(so_file)}: ALIGNED (#{alignment_status[:alignment]})\e[0m"
|
437
|
+
else
|
438
|
+
puts "\e[31m✗ #{File.basename(so_file)}: UNALIGNED (#{alignment_status[:alignment]})\e[0m"
|
439
|
+
unaligned_libs << {
|
440
|
+
file: so_file,
|
441
|
+
alignment: alignment_status[:alignment],
|
442
|
+
architecture: alignment_status[:architecture]
|
443
|
+
}
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
result.unaligned_libs = unaligned_libs
|
448
|
+
|
449
|
+
# 检查是否有 arm64-v8a 或 x86_64 架构的未对齐库
|
450
|
+
critical_unaligned = unaligned_libs.select do |lib|
|
451
|
+
arch = lib[:architecture]
|
452
|
+
arch == 'arm64-v8a' || arch == 'x86_64'
|
453
|
+
end
|
454
|
+
|
455
|
+
if critical_unaligned.empty?
|
456
|
+
result.elf_alignment_compliant = true
|
457
|
+
puts "\e[32m✓ 所有关键架构 (arm64-v8a/x86_64) 的共享库都已正确对齐\e[0m"
|
458
|
+
else
|
459
|
+
result.elf_alignment_compliant = false
|
460
|
+
critical_unaligned.each do |lib|
|
461
|
+
result.add_issue("#{lib[:architecture]} 架构的共享库 #{File.basename(lib[:file])} 未对齐 (16KB 页面大小要求)")
|
462
|
+
end
|
463
|
+
puts "\e[31m✗ 发现 #{critical_unaligned.length} 个关键架构的未对齐共享库\e[0m"
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
# 查找共享库文件
|
468
|
+
def self.find_shared_libraries(temp_dir)
|
469
|
+
so_files = []
|
470
|
+
|
471
|
+
# 在 temp_dir 中查找所有 .so 文件
|
472
|
+
Dir.glob(File.join(temp_dir, "**", "*.so")).each do |so_file|
|
473
|
+
so_files << so_file
|
474
|
+
end
|
475
|
+
|
476
|
+
# 也检查 base.apk 中的库文件
|
477
|
+
base_apk_path = File.join(temp_dir, "base.apk")
|
478
|
+
if File.exist?(base_apk_path)
|
479
|
+
base_temp_dir = nil
|
480
|
+
begin
|
481
|
+
base_temp_dir = Dir.mktmpdir("base_apk_libs_")
|
482
|
+
|
483
|
+
# 解压 base.apk 中的 lib 目录
|
484
|
+
if system("unzip", "-q", base_apk_path, "lib/*", "-d", base_temp_dir)
|
485
|
+
Dir.glob(File.join(base_temp_dir, "lib", "**", "*.so")).each do |so_file|
|
486
|
+
so_files << so_file
|
487
|
+
end
|
488
|
+
end
|
489
|
+
ensure
|
490
|
+
FileUtils.rm_rf(base_temp_dir) if base_temp_dir && File.directory?(base_temp_dir)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
so_files.uniq
|
495
|
+
end
|
496
|
+
|
497
|
+
# 检查单个 ELF 文件的对齐状态
|
498
|
+
def self.check_elf_alignment(so_file)
|
499
|
+
begin
|
500
|
+
# 首先检查文件是否为有效的 ELF 文件
|
501
|
+
file_output = `file "#{so_file}" 2>/dev/null`
|
502
|
+
unless file_output && file_output.include?('ELF')
|
503
|
+
return {
|
504
|
+
aligned: false,
|
505
|
+
alignment: "not_elf",
|
506
|
+
architecture: determine_architecture(so_file)
|
507
|
+
}
|
508
|
+
end
|
509
|
+
|
510
|
+
# 使用 objdump 检查 ELF 文件的对齐
|
511
|
+
unless tool_available?('objdump')
|
512
|
+
puts "objdump 不可用,无法检查 ELF 对齐"
|
513
|
+
return {
|
514
|
+
aligned: false,
|
515
|
+
alignment: "tool_missing",
|
516
|
+
architecture: determine_architecture(so_file)
|
517
|
+
}
|
518
|
+
end
|
519
|
+
|
520
|
+
objdump_output = `objdump -p "#{so_file}" 2>/dev/null`
|
521
|
+
|
522
|
+
if objdump_output && !objdump_output.empty?
|
523
|
+
# 查找 LOAD 段的对齐信息
|
524
|
+
load_sections = objdump_output.lines.select { |line| line.include?('LOAD') }
|
525
|
+
|
526
|
+
if !load_sections.empty?
|
527
|
+
# 获取第一个 LOAD 段的对齐值
|
528
|
+
first_load = load_sections.first
|
529
|
+
# 更精确的正则表达式匹配对齐值
|
530
|
+
alignment_match = first_load.match(/LOAD\s+off\s+0x[0-9a-f]+\s+vaddr\s+0x[0-9a-f]+\s+paddr\s+0x[0-9a-f]+\s+align\s+2\*\*(\d+)/)
|
531
|
+
|
532
|
+
if alignment_match
|
533
|
+
alignment_power = alignment_match[1].to_i
|
534
|
+
alignment_value = 2 ** alignment_power
|
535
|
+
|
536
|
+
# 检查是否满足 16KB 对齐要求 (2^14 = 16384)
|
537
|
+
aligned = alignment_value >= 16384
|
538
|
+
|
539
|
+
# 确定架构
|
540
|
+
architecture = determine_architecture(so_file)
|
541
|
+
|
542
|
+
return {
|
543
|
+
aligned: aligned,
|
544
|
+
alignment: "2^#{alignment_power}",
|
545
|
+
architecture: architecture
|
546
|
+
}
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
# 如果 objdump 失败,尝试使用 readelf
|
552
|
+
unless tool_available?('readelf')
|
553
|
+
puts "readelf 不可用,无法检查 ELF 对齐"
|
554
|
+
return {
|
555
|
+
aligned: false,
|
556
|
+
alignment: "tool_missing",
|
557
|
+
architecture: determine_architecture(so_file)
|
558
|
+
}
|
559
|
+
end
|
560
|
+
|
561
|
+
readelf_output = `readelf -l "#{so_file}" 2>/dev/null`
|
562
|
+
if readelf_output && !readelf_output.empty?
|
563
|
+
# 查找 LOAD 段的对齐信息
|
564
|
+
load_sections = readelf_output.lines.select { |line| line.include?('LOAD') }
|
565
|
+
|
566
|
+
if !load_sections.empty?
|
567
|
+
first_load = load_sections.first
|
568
|
+
alignment_match = first_load.match(/LOAD\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+0x[0-9a-f]+\s+(\d+)/)
|
569
|
+
|
570
|
+
if alignment_match
|
571
|
+
alignment_value = alignment_match[1].to_i
|
572
|
+
alignment_power = Math.log2(alignment_value).to_i if alignment_value > 0
|
573
|
+
|
574
|
+
aligned = alignment_value >= 16384
|
575
|
+
architecture = determine_architecture(so_file)
|
576
|
+
|
577
|
+
return {
|
578
|
+
aligned: aligned,
|
579
|
+
alignment: alignment_value > 0 ? "2^#{alignment_power}" : "0",
|
580
|
+
architecture: architecture
|
581
|
+
}
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# 如果所有方法都失败,返回未对齐状态
|
587
|
+
return {
|
588
|
+
aligned: false,
|
589
|
+
alignment: "unknown",
|
590
|
+
architecture: determine_architecture(so_file)
|
591
|
+
}
|
592
|
+
|
593
|
+
rescue => e
|
594
|
+
puts "检查 ELF 对齐失败 #{so_file}: #{e.message}"
|
595
|
+
return {
|
596
|
+
aligned: false,
|
597
|
+
alignment: "error",
|
598
|
+
architecture: determine_architecture(so_file)
|
599
|
+
}
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
# 确定共享库的架构
|
604
|
+
def self.determine_architecture(so_file)
|
605
|
+
# 从文件路径中提取架构信息
|
606
|
+
if so_file.include?('/arm64-v8a/')
|
607
|
+
'arm64-v8a'
|
608
|
+
elsif so_file.include?('/x86_64/')
|
609
|
+
'x86_64'
|
610
|
+
elsif so_file.include?('/armeabi-v7a/')
|
611
|
+
'armeabi-v7a'
|
612
|
+
elsif so_file.include?('/x86/')
|
613
|
+
'x86'
|
614
|
+
elsif so_file.include?('/armeabi/')
|
615
|
+
'armeabi'
|
616
|
+
else
|
617
|
+
'unknown'
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
# 打印合规检测摘要
|
622
|
+
def self.print_compliance_summary(result)
|
623
|
+
puts "\n\e[1m=== Google Play 合规检测摘要 ===\e[0m"
|
624
|
+
|
625
|
+
# AAB 包体积检测结果
|
626
|
+
if result.size_compliant
|
627
|
+
puts "\e[32m✓ AAB 包体积: #{result.aab_size_mb} MB (符合 Google Play 限制)\e[0m"
|
628
|
+
else
|
629
|
+
puts "\e[31m✗ AAB 包体积: #{result.aab_size_mb} MB (超出 Google Play 限制)\e[0m"
|
630
|
+
end
|
631
|
+
|
632
|
+
# Target SDK 检测结果
|
633
|
+
if result.target_sdk_compliant
|
634
|
+
puts "\e[32m✓ Target SDK: #{result.target_sdk_version} (符合 Android 15 要求)\e[0m"
|
635
|
+
else
|
636
|
+
puts "\e[31m✗ Target SDK: #{result.target_sdk_version} (需要至少 35)\e[0m"
|
637
|
+
end
|
638
|
+
|
639
|
+
# ELF 对齐检测结果
|
640
|
+
if result.elf_alignment_compliant
|
641
|
+
puts "\e[32m✓ ELF 对齐: 所有关键架构共享库已正确对齐 (16KB 页面大小)\e[0m"
|
642
|
+
else
|
643
|
+
puts "\e[31m✗ ELF 对齐: 发现 #{result.unaligned_libs.length} 个未对齐的共享库\e[0m"
|
644
|
+
end
|
645
|
+
|
646
|
+
# 总体合规状态
|
647
|
+
puts "\n\e[1m--- 总体合规状态 ---\e[0m"
|
648
|
+
if result.compliant?
|
649
|
+
puts "\e[32m\e[1m✓ 符合 Google Play 最新合规要求,可以正常提交\e[0m"
|
650
|
+
else
|
651
|
+
puts "\e[31m\e[1m✗ 不符合 Google Play 合规要求,需要修复以下问题:\e[0m"
|
652
|
+
result.compliance_issues.each do |issue|
|
653
|
+
puts "\e[31m - #{issue}\e[0m"
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
# 警告信息
|
658
|
+
if !result.warnings.empty?
|
659
|
+
puts "\n--- 警告信息 ---"
|
660
|
+
result.warnings.each do |warning|
|
661
|
+
puts "\e[33m ⚠ #{warning}\e[0m"
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
puts "================================"
|
666
|
+
end
|
667
|
+
end
|
668
|
+
end
|