pindo 5.18.9 → 5.18.12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3b74ddb66022968993f331b407f3a50a41d94a827e15e75f75dac1b8d5aadc1
4
- data.tar.gz: '08c5e2e855359f4369e02cb6cab841d38e3344794a7d4e8f9b0f2695683d1da2'
3
+ metadata.gz: 2f12654249c9833d657aadadbc44e7e7b749dbd590018855ddc196776dd21d85
4
+ data.tar.gz: 0bc9ef08ddfaefeeca63cb52380eace72853a9201e1abd828e5100b5de12cba0
5
5
  SHA512:
6
- metadata.gz: 6c1c36ddcf83fcff151bdd21e8e3104eec862b5c72aa15303a8fe3cae4f9be0dd36f37e2cee24d293e6ace76f895c72f057a18f12485c6d4a08905bb49350e21
7
- data.tar.gz: 1f3a484ff340bd398d150960753bb541103aca2b5a8ba30e712733c029e209c9fdfcb7db6c2d80b7c1bb1e1c8202d3319600ab12d23c4cb928c3f55c2390e74c
6
+ metadata.gz: 3a70f8ef90da1b660e40c0c1d84a56c2701fc541e8c932ac5facf224f3df59b4902cb45e5af246dfa492b311e3aa65d9cd9265d741fd2e514933a97c3dffe125
7
+ data.tar.gz: 6b867becf5698dcfec5b87c4cf5cb302b3c3292a0abe8d5bd890f69e15dd00abb2c63de3d461d5ca9e2cc1cb953050a0bb37f1558c7b01de25793764dbea22c2
@@ -0,0 +1,277 @@
1
+ module Pindo
2
+ class Command
3
+ class Android < Command
4
+ class Install < Android
5
+
6
+ self.summary = '安装 APK 到已连接的 Android 设备'
7
+
8
+ self.description = <<-DESC
9
+ 自动检测已连接的 Android 设备,将 APK 安装到设备上。
10
+
11
+ 支持三种方式指定 APK 文件:
12
+ 1. 命令行参数直接传入文件路径
13
+ 2. --apk 选项指定文件路径
14
+ 3. 不指定时自动搜索当前项目中最新的 APK 文件
15
+
16
+ 如果检测到多台可用设备,会列出设备列表让用户选择。
17
+
18
+ 示例:
19
+ $ pindo and install # 自动搜索 APK 并安装
20
+ $ pindo and install path/to/app.apk # 指定 APK 文件安装
21
+ $ pindo and install --apk=path/to/app.apk # 通过选项指定 APK 文件
22
+ DESC
23
+
24
+ self.arguments = [
25
+ CLAide::Argument.new('path/to/demo.apk', false),
26
+ ]
27
+
28
+ def self.option_items
29
+ @option_items ||= Pindo::Options::OptionGroup.merge(
30
+ Pindo::Options::ToolOptions.select(:apk)
31
+ )
32
+ end
33
+
34
+ def self.options
35
+ option_items.map(&:to_claide_option).concat(super)
36
+ end
37
+
38
+ def initialize(argv)
39
+ @args_apk_file = argv.shift_argument
40
+ @options = initialize_options(argv)
41
+
42
+ apk_option = @options[:apk] rescue nil
43
+ if !apk_option.nil?
44
+ @args_apk_file = apk_option
45
+ end
46
+ if @args_apk_file && !@args_apk_file.empty?
47
+ @args_apk_file = @args_apk_file.strip.gsub(/\"/, '')
48
+ end
49
+
50
+ super(argv)
51
+ end
52
+
53
+ def validate!
54
+ super
55
+ end
56
+
57
+ def run
58
+ # 1. 查找 adb
59
+ @adb_path = find_adb
60
+ puts "ADB: #{@adb_path}"
61
+
62
+ # 2. 查找 APK 文件
63
+ apk_file = find_apk_file
64
+ puts "APK 文件: #{apk_file}"
65
+
66
+ # 3. 检测可用设备
67
+ devices = detect_available_devices
68
+ if devices.empty?
69
+ raise Informative, "未检测到可用的 Android 设备,请确认设备已连接并开启 USB 调试"
70
+ end
71
+
72
+ # 4. 选择设备
73
+ device = select_device(devices)
74
+ puts "目标设备: #{device[:brand]} #{device[:model]} (#{device[:serial]})"
75
+
76
+ # 5. 安装 APK
77
+ install_apk_to_device(apk_file, device)
78
+ end
79
+
80
+ private
81
+
82
+ # 查找 adb 路径
83
+ def find_adb
84
+ # 1. PATH 中查找
85
+ adb_in_path = `which adb 2>/dev/null`.strip
86
+ return adb_in_path unless adb_in_path.empty?
87
+
88
+ # 2. 通过 SDK 路径查找(复用 android_project_helper 的逻辑)
89
+ sdk_dir = find_android_sdk_dir
90
+ if sdk_dir
91
+ adb = File.join(sdk_dir, "platform-tools", "adb")
92
+ return adb if File.executable?(adb)
93
+ end
94
+
95
+ raise Informative, "未找到 adb,请确认已安装 Android SDK 并配置 ANDROID_HOME 环境变量"
96
+ end
97
+
98
+ # 查找 Android SDK 目录
99
+ def find_android_sdk_dir
100
+ # Android Studio 默认路径
101
+ default_sdk = File.expand_path("~/Library/Android/sdk")
102
+ return default_sdk if File.directory?(default_sdk)
103
+
104
+ # 环境变量
105
+ sdk_dir = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT']
106
+ return sdk_dir if sdk_dir && File.directory?(sdk_dir)
107
+
108
+ # 从当前项目的 local.properties 读取
109
+ project_dir = Dir.pwd
110
+ ["local.properties", "Unity/local.properties"].each do |props_file|
111
+ props_path = File.join(project_dir, props_file)
112
+ if File.exist?(props_path)
113
+ content = File.read(props_path)
114
+ if content =~ /sdk\.dir\s*=\s*(.+)/
115
+ dir = $1.strip
116
+ return dir if File.directory?(dir)
117
+ end
118
+ end
119
+ end
120
+
121
+ nil
122
+ end
123
+
124
+ # 查找 APK 文件
125
+ def find_apk_file
126
+ # 优先使用用户指定的文件
127
+ if @args_apk_file && !@args_apk_file.empty?
128
+ apk_path = File.expand_path(@args_apk_file)
129
+ unless File.exist?(apk_path)
130
+ raise Informative, "APK 文件不存在: #{apk_path}"
131
+ end
132
+ return apk_path
133
+ end
134
+
135
+ # 自动搜索
136
+ project_dir = Dir.pwd
137
+ apk_file = nil
138
+
139
+ require 'pindo/module/build/build_helper'
140
+ build_helper = Pindo::BuildHelper.share_instance
141
+ project_type = build_helper.project_type(project_dir)
142
+
143
+ case project_type
144
+ when :android
145
+ apk_file = find_android_apk(project_dir)
146
+ when :unity
147
+ apk_file = find_unity_android_apk(project_dir)
148
+ end
149
+
150
+ # 兜底搜索
151
+ if apk_file.nil?
152
+ search_paths = [
153
+ File.join(project_dir, "build", "**", "*.apk"),
154
+ File.join(project_dir, "*.apk")
155
+ ]
156
+ search_paths.each do |pattern|
157
+ found = Dir.glob(pattern).max_by { |f| File.mtime(f) }
158
+ if found
159
+ apk_file = found
160
+ break
161
+ end
162
+ end
163
+ end
164
+
165
+ if apk_file.nil?
166
+ raise Informative, "未找到 APK 文件,请指定文件路径: pindo and install path/to/app.apk"
167
+ end
168
+
169
+ apk_file
170
+ end
171
+
172
+ # 在 Android 工程中查找最新 APK
173
+ def find_android_apk(project_dir)
174
+ build_path = File.join(project_dir, "build", "**", "*.apk")
175
+ Dir.glob(build_path).max_by { |f| File.mtime(f) }
176
+ end
177
+
178
+ # 在 Unity 工程中查找最新 Android APK
179
+ def find_unity_android_apk(project_dir)
180
+ apk_files = []
181
+ ["GoodPlatform/BaseAndroid/build", "GoodPlatform/Android/build"].each do |sub_dir|
182
+ dir = File.join(project_dir, sub_dir)
183
+ if File.exist?(dir)
184
+ apk_files.concat(Dir.glob(File.join(dir, "**", "*.apk")))
185
+ end
186
+ end
187
+ apk_files.max_by { |f| File.mtime(f) } if apk_files.any?
188
+ end
189
+
190
+ # 检测已连接的 Android 设备
191
+ def detect_available_devices
192
+ output = `"#{@adb_path}" devices 2>/dev/null`.to_s
193
+ devices = []
194
+
195
+ output.split("\n").each do |line|
196
+ next if line.start_with?("List of devices")
197
+ next if line.strip.empty?
198
+ next if line.start_with?("*")
199
+
200
+ parts = line.strip.split(/\s+/)
201
+ next if parts.size < 2
202
+
203
+ serial = parts[0]
204
+ state = parts[1]
205
+
206
+ # 只保留 device 状态(排除 offline、unauthorized、no permissions 等)
207
+ next unless state == "device"
208
+
209
+ brand = get_device_prop(serial, "ro.product.brand")
210
+ model = get_device_prop(serial, "ro.product.model")
211
+ android_version = get_device_prop(serial, "ro.build.version.release")
212
+
213
+ devices << {
214
+ serial: serial,
215
+ brand: brand.capitalize,
216
+ model: model,
217
+ android_version: android_version
218
+ }
219
+ end
220
+
221
+ devices
222
+ end
223
+
224
+ # 获取设备属性
225
+ def get_device_prop(serial, prop)
226
+ `"#{@adb_path}" -s #{serial} shell getprop #{prop} 2>/dev/null`.strip
227
+ end
228
+
229
+ # 选择设备
230
+ def select_device(devices)
231
+ if devices.size == 1
232
+ return devices.first
233
+ end
234
+
235
+ puts "\n检测到 #{devices.size} 台可用设备:"
236
+ devices.each_with_index do |device, index|
237
+ puts " [#{index + 1}] #{device[:brand]} #{device[:model]} (Android #{device[:android_version]}) - #{device[:serial]}"
238
+ end
239
+
240
+ selection = nil
241
+ loop do
242
+ print "\n请选择设备 [1-#{devices.size}]: "
243
+ input = $stdin.gets
244
+ if input.nil?
245
+ raise Informative, "用户取消操作"
246
+ end
247
+ input = input.strip
248
+ num = input.to_i
249
+ if num >= 1 && num <= devices.size
250
+ selection = num - 1
251
+ break
252
+ end
253
+ puts "无效输入,请输入 1-#{devices.size} 之间的数字"
254
+ end
255
+
256
+ devices[selection]
257
+ end
258
+
259
+ # 安装 APK 到设备
260
+ def install_apk_to_device(apk_file, device)
261
+ puts "\n正在安装到 #{device[:brand]} #{device[:model]}..."
262
+
263
+ output = `"#{@adb_path}" -s #{device[:serial]} install -r "#{apk_file}" 2>&1`
264
+ exit_code = $?.exitstatus
265
+
266
+ if exit_code == 0 && output.include?("Success")
267
+ puts "安装成功!"
268
+ else
269
+ puts output unless output.strip.empty?
270
+ raise Informative, "安装失败"
271
+ end
272
+ end
273
+
274
+ end
275
+ end
276
+ end
277
+ end
@@ -2,6 +2,7 @@
2
2
  require 'pindo/command/android/autobuild'
3
3
  require 'pindo/command/android/autoresign'
4
4
  require 'pindo/command/android/keystore'
5
+ require 'pindo/command/android/install'
5
6
 
6
7
 
7
8
  module Pindo
@@ -0,0 +1,241 @@
1
+ module Pindo
2
+ class Command
3
+ class Ios < Command
4
+ class Install < Ios
5
+
6
+ self.summary = '安装 IPA 到已连接的 iOS 真机设备'
7
+
8
+ self.description = <<-DESC
9
+ 自动检测已连接的 iPhone/iPad 设备,将 IPA 安装到设备上。
10
+
11
+ 支持三种方式指定 IPA 文件:
12
+ 1. 命令行参数直接传入文件路径
13
+ 2. --ipa 选项指定文件路径
14
+ 3. 不指定时自动搜索当前项目中最新的 IPA 文件
15
+
16
+ 如果检测到多台可用设备,会列出设备列表让用户选择。
17
+
18
+ 示例:
19
+ $ pindo ios install # 自动搜索 IPA 并安装
20
+ $ pindo ios install path/to/app.ipa # 指定 IPA 文件安装
21
+ $ pindo ios install --ipa=path/to/app.ipa # 通过选项指定 IPA 文件
22
+ DESC
23
+
24
+ self.arguments = [
25
+ CLAide::Argument.new('path/to/demo.ipa', false),
26
+ ]
27
+
28
+ def self.option_items
29
+ @option_items ||= Pindo::Options::OptionGroup.merge(
30
+ Pindo::Options::ToolOptions.select(:ipa)
31
+ )
32
+ end
33
+
34
+ def self.options
35
+ option_items.map(&:to_claide_option).concat(super)
36
+ end
37
+
38
+ def initialize(argv)
39
+ @args_ipa_file = argv.shift_argument
40
+ @options = initialize_options(argv)
41
+
42
+ ipa_option = @options[:ipa]
43
+ if !ipa_option.nil?
44
+ @args_ipa_file = ipa_option
45
+ end
46
+ if @args_ipa_file && !@args_ipa_file.empty?
47
+ @args_ipa_file = @args_ipa_file.strip.gsub(/\"/, '')
48
+ end
49
+
50
+ super(argv)
51
+ end
52
+
53
+ def validate!
54
+ super
55
+ end
56
+
57
+ def run
58
+ # 1. 查找 IPA 文件
59
+ ipa_file = find_ipa_file
60
+ puts "IPA 文件: #{ipa_file}"
61
+
62
+ # 2. 检测可用设备
63
+ devices = detect_available_devices
64
+ if devices.empty?
65
+ raise Informative, "未检测到可用的 iPhone/iPad 设备,请确认设备已连接并信任此电脑"
66
+ end
67
+
68
+ # 3. 选择设备
69
+ device = select_device(devices)
70
+ puts "目标设备: #{device[:name]} (#{device[:model]})"
71
+
72
+ # 4. 安装 IPA
73
+ install_ipa_to_device(ipa_file, device)
74
+ end
75
+
76
+ private
77
+
78
+ # 查找 IPA 文件
79
+ def find_ipa_file
80
+ # 优先使用用户指定的文件
81
+ if @args_ipa_file && !@args_ipa_file.empty?
82
+ ipa_path = File.expand_path(@args_ipa_file)
83
+ unless File.exist?(ipa_path)
84
+ raise Informative, "IPA 文件不存在: #{ipa_path}"
85
+ end
86
+ return ipa_path
87
+ end
88
+
89
+ # 自动搜索
90
+ project_dir = Dir.pwd
91
+ ipa_file = nil
92
+
93
+ # 检测项目类型并搜索
94
+ require 'pindo/module/build/build_helper'
95
+ build_helper = Pindo::BuildHelper.share_instance
96
+ project_type = build_helper.project_type(project_dir)
97
+
98
+ case project_type
99
+ when :ios
100
+ ipa_file = find_ios_ipa(project_dir)
101
+ when :unity
102
+ ipa_file = find_unity_ios_ipa(project_dir)
103
+ end
104
+
105
+ # 兜底:在当前目录和 build 目录查找
106
+ if ipa_file.nil?
107
+ search_paths = [
108
+ File.join(project_dir, "build", "*.ipa"),
109
+ File.join(project_dir, "*.ipa")
110
+ ]
111
+ search_paths.each do |pattern|
112
+ found = Dir.glob(pattern).max_by { |f| File.mtime(f) }
113
+ if found
114
+ ipa_file = found
115
+ break
116
+ end
117
+ end
118
+ end
119
+
120
+ if ipa_file.nil?
121
+ raise Informative, "未找到 IPA 文件,请指定文件路径: pindo ios install path/to/app.ipa"
122
+ end
123
+
124
+ ipa_file
125
+ end
126
+
127
+ # 在 iOS 工程中查找最新 IPA
128
+ def find_ios_ipa(project_dir)
129
+ build_path = File.join(project_dir, "build", "*.ipa")
130
+ Dir.glob(build_path).max_by { |f| File.mtime(f) }
131
+ end
132
+
133
+ # 在 Unity 工程中查找最新 iOS IPA
134
+ def find_unity_ios_ipa(project_dir)
135
+ ipa_files = []
136
+ ["GoodPlatform/BaseiOS/build", "GoodPlatform/iOS/build"].each do |sub_dir|
137
+ dir = File.join(project_dir, sub_dir)
138
+ if File.exist?(dir)
139
+ ipa_files.concat(Dir.glob(File.join(dir, "*.ipa")))
140
+ end
141
+ end
142
+ ipa_files.max_by { |f| File.mtime(f) } if ipa_files.any?
143
+ end
144
+
145
+ # 检测可用的 iPhone/iPad 设备
146
+ def detect_available_devices
147
+ require 'json'
148
+ require 'tempfile'
149
+
150
+ # 使用 JSON 输出避免文本解析对齐问题
151
+ json_tmpfile = Tempfile.new(['devicectl', '.json'])
152
+ json_path = json_tmpfile.path
153
+ json_tmpfile.close
154
+
155
+ system("xcrun devicectl list devices --json-output \"#{json_path}\" >/dev/null 2>&1")
156
+
157
+ unless File.exist?(json_path) && File.size(json_path) > 0
158
+ json_tmpfile.unlink rescue nil
159
+ raise Informative, "无法执行 xcrun devicectl,请确认 Xcode 已安装"
160
+ end
161
+
162
+ json_data = JSON.parse(File.read(json_path))
163
+ json_tmpfile.unlink rescue nil
164
+
165
+ devices = []
166
+ device_list = json_data.dig('result', 'devices') || []
167
+
168
+ device_list.each do |device|
169
+ tunnel_state = device.dig('connectionProperties', 'tunnelState') || ''
170
+ device_type = device.dig('hardwareProperties', 'deviceType') || ''
171
+ name = device.dig('deviceProperties', 'name') || ''
172
+ identifier = device['identifier'] || ''
173
+ model = device.dig('hardwareProperties', 'marketingName') || ''
174
+ product_type = device.dig('hardwareProperties', 'productType') || ''
175
+
176
+ # 只保留 available(tunnelState 不是 unavailable)的 iPhone/iPad
177
+ next if tunnel_state == 'unavailable'
178
+ next unless device_type == 'iPhone' || device_type == 'iPad'
179
+
180
+ devices << {
181
+ name: name,
182
+ identifier: identifier,
183
+ model: "#{model} (#{product_type})",
184
+ device_type: device_type
185
+ }
186
+ end
187
+
188
+ devices
189
+ end
190
+
191
+ # 选择设备
192
+ def select_device(devices)
193
+ if devices.size == 1
194
+ return devices.first
195
+ end
196
+
197
+ # 多台设备,让用户选择
198
+ puts "\n检测到 #{devices.size} 台可用设备:"
199
+ devices.each_with_index do |device, index|
200
+ puts " [#{index + 1}] #{device[:name]} - #{device[:model]}"
201
+ end
202
+
203
+ selection = nil
204
+ loop do
205
+ print "\n请选择设备 [1-#{devices.size}]: "
206
+ input = $stdin.gets
207
+ if input.nil?
208
+ raise Informative, "用户取消操作"
209
+ end
210
+ input = input.strip
211
+ num = input.to_i
212
+ if num >= 1 && num <= devices.size
213
+ selection = num - 1
214
+ break
215
+ end
216
+ puts "无效输入,请输入 1-#{devices.size} 之间的数字"
217
+ end
218
+
219
+ devices[selection]
220
+ end
221
+
222
+ # 安装 IPA 到设备
223
+ def install_ipa_to_device(ipa_file, device)
224
+ puts "\n正在安装到 #{device[:name]}..."
225
+
226
+ cmd = "xcrun devicectl device install app --device #{device[:identifier]} \"#{ipa_file}\" 2>&1"
227
+ output = `#{cmd}`
228
+ exit_code = $?.exitstatus
229
+
230
+ if exit_code == 0
231
+ puts "安装成功!"
232
+ else
233
+ puts output unless output.strip.empty?
234
+ raise Informative, "安装失败 (exit code: #{exit_code})"
235
+ end
236
+ end
237
+
238
+ end
239
+ end
240
+ end
241
+ end
@@ -8,6 +8,7 @@ require 'pindo/command/ios/podlint'
8
8
  require 'pindo/command/ios/podpush'
9
9
  require 'pindo/command/ios/podupdate'
10
10
  require 'pindo/command/ios/fixproj'
11
+ require 'pindo/command/ios/install'
11
12
 
12
13
  module Pindo
13
14
  class Command
@@ -35,6 +35,7 @@ module Pindo
35
35
  puts "处理独立的 Unity 导出工程..."
36
36
  Pindo::GradleHelper.update_build_gradle(project_dir)
37
37
  Pindo::AndroidProjectHelper.add_unity_namespace(project_dir)
38
+ Pindo::AndroidProjectHelper.ensure_unity_il2cpp_jni_merge_depends_on!(project_dir)
38
39
  Pindo::AndroidProjectHelper.modify_il2cpp_config(project_dir)
39
40
  Pindo::AndroidProjectHelper.remove_desktop_google_service(project_dir)
40
41
 
@@ -61,6 +62,7 @@ module Pindo
61
62
 
62
63
  # Unity 特有的处理
63
64
  Pindo::AndroidProjectHelper.add_unity_namespace(unity_dir)
65
+ Pindo::AndroidProjectHelper.ensure_unity_il2cpp_jni_merge_depends_on!(unity_dir)
64
66
  Pindo::AndroidProjectHelper.modify_il2cpp_config(unity_dir)
65
67
  Pindo::AndroidProjectHelper.remove_desktop_google_service(unity_dir)
66
68
  end