mysigner 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/.DS_Store +0 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +137 -0
- data/LICENSE +201 -0
- data/MANUAL_TEST.md +341 -0
- data/README.md +493 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/mysigner +5 -0
- data/lib/mysigner/build/android_executor.rb +367 -0
- data/lib/mysigner/build/android_parser.rb +293 -0
- data/lib/mysigner/build/configurator.rb +126 -0
- data/lib/mysigner/build/detector.rb +388 -0
- data/lib/mysigner/build/error_analyzer.rb +193 -0
- data/lib/mysigner/build/executor.rb +176 -0
- data/lib/mysigner/build/parser.rb +206 -0
- data/lib/mysigner/cli/auth_commands.rb +1381 -0
- data/lib/mysigner/cli/build_commands.rb +2095 -0
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +500 -0
- data/lib/mysigner/cli/concerns/api_helpers.rb +131 -0
- data/lib/mysigner/cli/concerns/error_handlers.rb +446 -0
- data/lib/mysigner/cli/concerns/helpers.rb +63 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +1034 -0
- data/lib/mysigner/cli/resource_commands.rb +2670 -0
- data/lib/mysigner/cli.rb +43 -0
- data/lib/mysigner/client.rb +189 -0
- data/lib/mysigner/config.rb +311 -0
- data/lib/mysigner/export/exporter.rb +150 -0
- data/lib/mysigner/signing/certificate_checker.rb +148 -0
- data/lib/mysigner/signing/keystore_manager.rb +239 -0
- data/lib/mysigner/signing/validator.rb +150 -0
- data/lib/mysigner/signing/wizard.rb +784 -0
- data/lib/mysigner/upload/app_store_automation.rb +402 -0
- data/lib/mysigner/upload/app_store_submission.rb +312 -0
- data/lib/mysigner/upload/play_store_uploader.rb +378 -0
- data/lib/mysigner/upload/uploader.rb +373 -0
- data/lib/mysigner/version.rb +3 -0
- data/lib/mysigner.rb +15 -0
- data/mysigner.gemspec +78 -0
- data/test_manual.rb +102 -0
- metadata +286 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
|
|
4
|
+
module Mysigner
|
|
5
|
+
module Build
|
|
6
|
+
class AndroidExecutor
|
|
7
|
+
class BuildError < StandardError; end
|
|
8
|
+
|
|
9
|
+
def initialize(project_info, parser)
|
|
10
|
+
@project_info = project_info
|
|
11
|
+
@parser = parser
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Build AAB (Android App Bundle) for Play Store
|
|
15
|
+
# Returns: path to .aab file
|
|
16
|
+
# Options:
|
|
17
|
+
# - variant: Build variant (default: 'release')
|
|
18
|
+
# - keystore_path: Path to keystore file
|
|
19
|
+
# - keystore_password: Keystore password
|
|
20
|
+
# - key_alias: Key alias in keystore
|
|
21
|
+
# - key_password: Key password (defaults to keystore_password)
|
|
22
|
+
# - version_code: Override version code (passed via gradle property)
|
|
23
|
+
def build_aab!(variant: 'release', keystore_path: nil, keystore_password: nil, key_alias: nil, key_password: nil, version_code: nil)
|
|
24
|
+
@variant = variant
|
|
25
|
+
@keystore_path = keystore_path
|
|
26
|
+
@keystore_password = keystore_password
|
|
27
|
+
@key_alias = key_alias
|
|
28
|
+
@key_password = key_password || keystore_password
|
|
29
|
+
@version_code = version_code
|
|
30
|
+
|
|
31
|
+
# Determine task name
|
|
32
|
+
task = "bundle#{variant.capitalize}"
|
|
33
|
+
|
|
34
|
+
# Build
|
|
35
|
+
success = run_gradle_build(task)
|
|
36
|
+
|
|
37
|
+
unless success
|
|
38
|
+
raise BuildError, "Android build failed. Check output above for errors."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Find output AAB
|
|
42
|
+
aab_path = find_aab_output(variant)
|
|
43
|
+
|
|
44
|
+
unless aab_path && File.exist?(aab_path)
|
|
45
|
+
raise BuildError, "Build reported success but AAB not found. Expected at: #{@parser.aab_output_path(variant)}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
aab_path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Build APK
|
|
52
|
+
# Returns: path to .apk file
|
|
53
|
+
def build_apk!(variant: 'release', keystore_path: nil, keystore_password: nil, key_alias: nil, key_password: nil)
|
|
54
|
+
@variant = variant
|
|
55
|
+
@keystore_path = keystore_path
|
|
56
|
+
@keystore_password = keystore_password
|
|
57
|
+
@key_alias = key_alias
|
|
58
|
+
@key_password = key_password || keystore_password
|
|
59
|
+
|
|
60
|
+
# Determine task name
|
|
61
|
+
task = "assemble#{variant.capitalize}"
|
|
62
|
+
|
|
63
|
+
# Build
|
|
64
|
+
success = run_gradle_build(task)
|
|
65
|
+
|
|
66
|
+
unless success
|
|
67
|
+
raise BuildError, "Android build failed. Check output above for errors."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Find output APK
|
|
71
|
+
apk_path = find_apk_output(variant)
|
|
72
|
+
|
|
73
|
+
unless apk_path && File.exist?(apk_path)
|
|
74
|
+
raise BuildError, "Build reported success but APK not found. Expected at: #{@parser.apk_output_path(variant)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
apk_path
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Clean build outputs
|
|
81
|
+
def clean!
|
|
82
|
+
run_gradle_command('clean')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def run_gradle_build(task)
|
|
88
|
+
# Ensure JAVA_HOME is valid
|
|
89
|
+
ensure_java_home!
|
|
90
|
+
|
|
91
|
+
# Handle framework-specific pre-build steps
|
|
92
|
+
run_pre_build_steps
|
|
93
|
+
|
|
94
|
+
# Build command with signing properties
|
|
95
|
+
cmd = build_gradle_command(task)
|
|
96
|
+
|
|
97
|
+
# Execute build
|
|
98
|
+
execute_with_output(cmd)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def ensure_java_home!
|
|
102
|
+
java_home = ENV['JAVA_HOME']
|
|
103
|
+
|
|
104
|
+
# Check if JAVA_HOME is set and valid
|
|
105
|
+
if java_home && !java_home.empty? && Dir.exist?(java_home)
|
|
106
|
+
# All good
|
|
107
|
+
else
|
|
108
|
+
# Try to detect valid JAVA_HOME
|
|
109
|
+
detected = detect_java_home
|
|
110
|
+
if detected
|
|
111
|
+
puts "🔧 Auto-detected JAVA_HOME: #{detected}"
|
|
112
|
+
ENV['JAVA_HOME'] = detected
|
|
113
|
+
elsif java_home && !java_home.empty?
|
|
114
|
+
raise BuildError, "JAVA_HOME is set to invalid directory: #{java_home}\n" \
|
|
115
|
+
"Run 'mysigner doctor' to fix, or set JAVA_HOME manually:\n" \
|
|
116
|
+
" export JAVA_HOME=$(/usr/libexec/java_home -v 17)"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Also ensure ANDROID_HOME is set
|
|
121
|
+
ensure_android_home!
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ensure_android_home!
|
|
125
|
+
android_home = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT']
|
|
126
|
+
|
|
127
|
+
# Check if already valid
|
|
128
|
+
if android_home && !android_home.empty? && Dir.exist?(android_home)
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Try to detect Android SDK
|
|
133
|
+
detected = detect_android_home
|
|
134
|
+
if detected
|
|
135
|
+
puts "🔧 Auto-detected ANDROID_HOME: #{detected}"
|
|
136
|
+
ENV['ANDROID_HOME'] = detected
|
|
137
|
+
ENV['ANDROID_SDK_ROOT'] = detected
|
|
138
|
+
else
|
|
139
|
+
raise BuildError, "Android SDK not found.\n" \
|
|
140
|
+
"Run 'mysigner doctor' to diagnose, or set ANDROID_HOME:\n" \
|
|
141
|
+
" export ANDROID_HOME=~/Library/Android/sdk"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def detect_android_home
|
|
146
|
+
# Common SDK locations
|
|
147
|
+
candidates = [
|
|
148
|
+
File.expand_path('~/Library/Android/sdk'),
|
|
149
|
+
File.expand_path('~/Android/Sdk'),
|
|
150
|
+
'/opt/homebrew/share/android-commandlinetools',
|
|
151
|
+
'/usr/local/share/android-commandlinetools'
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
candidates.each do |path|
|
|
155
|
+
return path if Dir.exist?(path) && Dir.exist?(File.join(path, 'platform-tools'))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def detect_java_home
|
|
162
|
+
# Try macOS java_home utility first
|
|
163
|
+
if system('which /usr/libexec/java_home > /dev/null 2>&1')
|
|
164
|
+
java_home = `/usr/libexec/java_home 2>/dev/null`.strip
|
|
165
|
+
return java_home if !java_home.empty? && Dir.exist?(java_home)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Try Homebrew paths (Apple Silicon)
|
|
169
|
+
%w[
|
|
170
|
+
/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
|
|
171
|
+
/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
|
|
172
|
+
/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home
|
|
173
|
+
].each { |p| return p if Dir.exist?(p) }
|
|
174
|
+
|
|
175
|
+
# Try Homebrew paths (Intel)
|
|
176
|
+
%w[
|
|
177
|
+
/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
|
|
178
|
+
/usr/local/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
|
|
179
|
+
/usr/local/opt/openjdk/libexec/openjdk.jdk/Contents/Home
|
|
180
|
+
].each { |p| return p if Dir.exist?(p) }
|
|
181
|
+
|
|
182
|
+
# Try system Java
|
|
183
|
+
system_paths = Dir.glob('/Library/Java/JavaVirtualMachines/*/Contents/Home')
|
|
184
|
+
return system_paths.first if system_paths.any?
|
|
185
|
+
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def run_pre_build_steps
|
|
190
|
+
case @project_info[:framework]
|
|
191
|
+
when :capacitor
|
|
192
|
+
# Capacitor: sync before build
|
|
193
|
+
puts "🔄 Syncing Capacitor..."
|
|
194
|
+
Dir.chdir(@project_info[:directory]) do
|
|
195
|
+
system('npx cap sync android > /dev/null 2>&1')
|
|
196
|
+
end
|
|
197
|
+
when :react_native
|
|
198
|
+
# React Native: might need to install dependencies
|
|
199
|
+
if File.exist?(File.join(@project_info[:directory], 'node_modules'))
|
|
200
|
+
# Dependencies already installed
|
|
201
|
+
else
|
|
202
|
+
puts "📦 Installing npm dependencies..."
|
|
203
|
+
Dir.chdir(@project_info[:directory]) do
|
|
204
|
+
system('npm install > /dev/null 2>&1') || system('yarn install > /dev/null 2>&1')
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
when :flutter
|
|
208
|
+
# Flutter: ensure dependencies are fetched
|
|
209
|
+
puts "📦 Getting Flutter dependencies..."
|
|
210
|
+
Dir.chdir(@project_info[:directory]) do
|
|
211
|
+
system('flutter pub get > /dev/null 2>&1')
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def build_gradle_command(task)
|
|
217
|
+
android_dir = @parser.android_directory
|
|
218
|
+
gradle_cmd = @parser.gradle_command
|
|
219
|
+
|
|
220
|
+
cmd_parts = []
|
|
221
|
+
|
|
222
|
+
# Export JAVA_HOME if we detected/fixed it
|
|
223
|
+
java_home = ENV['JAVA_HOME']
|
|
224
|
+
if java_home && Dir.exist?(java_home)
|
|
225
|
+
cmd_parts << "export JAVA_HOME=#{shell_escape(java_home)}"
|
|
226
|
+
cmd_parts << "&&"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Export ANDROID_HOME if we detected/fixed it
|
|
230
|
+
android_home = ENV['ANDROID_HOME']
|
|
231
|
+
if android_home && Dir.exist?(android_home)
|
|
232
|
+
cmd_parts << "export ANDROID_HOME=#{shell_escape(android_home)}"
|
|
233
|
+
cmd_parts << "&&"
|
|
234
|
+
cmd_parts << "export ANDROID_SDK_ROOT=#{shell_escape(android_home)}"
|
|
235
|
+
cmd_parts << "&&"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Change to android directory and run gradle
|
|
239
|
+
cmd_parts << "cd #{shell_escape(android_dir)}"
|
|
240
|
+
cmd_parts << "&&"
|
|
241
|
+
cmd_parts << gradle_cmd
|
|
242
|
+
cmd_parts << task
|
|
243
|
+
|
|
244
|
+
# Add signing properties if provided
|
|
245
|
+
if @keystore_path && File.exist?(@keystore_path)
|
|
246
|
+
cmd_parts << "-Pandroid.injected.signing.store.file=#{shell_escape(File.absolute_path(@keystore_path))}"
|
|
247
|
+
cmd_parts << "-Pandroid.injected.signing.store.password=#{shell_escape(@keystore_password)}" if @keystore_password
|
|
248
|
+
cmd_parts << "-Pandroid.injected.signing.key.alias=#{shell_escape(@key_alias)}" if @key_alias
|
|
249
|
+
cmd_parts << "-Pandroid.injected.signing.key.password=#{shell_escape(@key_password)}" if @key_password
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Add version code override if provided (no file modification needed)
|
|
253
|
+
if @version_code
|
|
254
|
+
cmd_parts << "-PversionCode=#{@version_code}"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Standard build options
|
|
258
|
+
cmd_parts << "--no-daemon" # Avoid daemon issues in CI
|
|
259
|
+
cmd_parts << "-q" # Quiet mode (less noise)
|
|
260
|
+
|
|
261
|
+
cmd_parts.join(' ')
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def run_gradle_command(task)
|
|
265
|
+
android_dir = @parser.android_directory
|
|
266
|
+
gradle_cmd = @parser.gradle_command
|
|
267
|
+
|
|
268
|
+
exports = []
|
|
269
|
+
java_home = ENV['JAVA_HOME']
|
|
270
|
+
exports << "export JAVA_HOME=#{shell_escape(java_home)}" if java_home && Dir.exist?(java_home)
|
|
271
|
+
|
|
272
|
+
android_home = ENV['ANDROID_HOME']
|
|
273
|
+
if android_home && Dir.exist?(android_home)
|
|
274
|
+
exports << "export ANDROID_HOME=#{shell_escape(android_home)}"
|
|
275
|
+
exports << "export ANDROID_SDK_ROOT=#{shell_escape(android_home)}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
export_str = exports.any? ? exports.join(' && ') + ' && ' : ''
|
|
279
|
+
cmd = "#{export_str}cd #{shell_escape(android_dir)} && #{gradle_cmd} #{task} --no-daemon -q"
|
|
280
|
+
system(cmd)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def execute_with_output(cmd)
|
|
284
|
+
puts "🏗️ Running: gradle #{@variant}..."
|
|
285
|
+
puts ""
|
|
286
|
+
|
|
287
|
+
# Run command and capture output in real-time
|
|
288
|
+
IO.popen("#{cmd} 2>&1", 'r') do |io|
|
|
289
|
+
io.each_line do |line|
|
|
290
|
+
next if line.strip.empty?
|
|
291
|
+
|
|
292
|
+
# Show errors and warnings
|
|
293
|
+
if line.include?('FAILURE') || line.include?('ERROR') || line.include?('error:')
|
|
294
|
+
puts line
|
|
295
|
+
elsif line.include?('warning:') || line.include?('WARNING')
|
|
296
|
+
puts line
|
|
297
|
+
# Show task progress
|
|
298
|
+
elsif line.include?('> Task') || line.include?('BUILD')
|
|
299
|
+
print '.'
|
|
300
|
+
# Show download progress
|
|
301
|
+
elsif line.include?('Download')
|
|
302
|
+
# Skip verbose download logs
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
puts "" # New line after dots
|
|
308
|
+
|
|
309
|
+
$?.success?
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def find_aab_output(variant)
|
|
313
|
+
android_dir = @parser.android_directory
|
|
314
|
+
|
|
315
|
+
# Search patterns for AAB files
|
|
316
|
+
patterns = [
|
|
317
|
+
File.join(android_dir, "app/build/outputs/bundle/#{variant}/*.aab"),
|
|
318
|
+
File.join(android_dir, "app/build/outputs/bundle/#{variant.downcase}/*.aab"),
|
|
319
|
+
File.join(android_dir, "build/outputs/bundle/#{variant}/*.aab"),
|
|
320
|
+
# Flutter uses different naming
|
|
321
|
+
File.join(android_dir, "app/build/outputs/bundle/#{variant}/app-#{variant}.aab"),
|
|
322
|
+
File.join(android_dir, "build/app/outputs/bundle/#{variant}/*.aab")
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
patterns.each do |pattern|
|
|
326
|
+
matches = Dir.glob(pattern)
|
|
327
|
+
return matches.first if matches.any?
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def find_apk_output(variant)
|
|
334
|
+
android_dir = @parser.android_directory
|
|
335
|
+
|
|
336
|
+
# Search patterns for APK files
|
|
337
|
+
patterns = [
|
|
338
|
+
File.join(android_dir, "app/build/outputs/apk/#{variant}/*.apk"),
|
|
339
|
+
File.join(android_dir, "app/build/outputs/apk/#{variant.downcase}/*.apk"),
|
|
340
|
+
File.join(android_dir, "build/outputs/apk/#{variant}/*.apk"),
|
|
341
|
+
File.join(android_dir, "app/build/outputs/apk/#{variant}/app-#{variant}.apk"),
|
|
342
|
+
# Unsigned APKs
|
|
343
|
+
File.join(android_dir, "app/build/outputs/apk/#{variant}/app-#{variant}-unsigned.apk")
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
patterns.each do |pattern|
|
|
347
|
+
matches = Dir.glob(pattern)
|
|
348
|
+
return matches.first if matches.any?
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
nil
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def shell_escape(str)
|
|
355
|
+
return "''" if str.nil? || str.empty?
|
|
356
|
+
|
|
357
|
+
# If string contains no special characters, return as-is
|
|
358
|
+
if str =~ /\A[a-zA-Z0-9_.\-\/]+\z/
|
|
359
|
+
return str
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Otherwise, quote it
|
|
363
|
+
"'" + str.gsub("'", "'\\''") + "'"
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
module Mysigner
|
|
2
|
+
module Build
|
|
3
|
+
class AndroidParser
|
|
4
|
+
attr_reader :project_info
|
|
5
|
+
|
|
6
|
+
def initialize(project_info)
|
|
7
|
+
@project_info = project_info
|
|
8
|
+
@gradle_content = read_gradle_file
|
|
9
|
+
@manifest_content = read_manifest_file
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get the application ID (package name)
|
|
13
|
+
def application_id
|
|
14
|
+
# Try to extract from build.gradle
|
|
15
|
+
app_id = extract_from_gradle('applicationId')
|
|
16
|
+
return app_id if app_id
|
|
17
|
+
|
|
18
|
+
# Try namespace (newer Gradle)
|
|
19
|
+
namespace = extract_from_gradle('namespace')
|
|
20
|
+
return namespace if namespace
|
|
21
|
+
|
|
22
|
+
# Try Expo app.json (for Expo projects)
|
|
23
|
+
expo_package = extract_from_expo_config
|
|
24
|
+
return expo_package if expo_package
|
|
25
|
+
|
|
26
|
+
# Fallback to AndroidManifest.xml
|
|
27
|
+
extract_package_from_manifest
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
alias package_name application_id
|
|
31
|
+
|
|
32
|
+
# Get version code
|
|
33
|
+
def version_code
|
|
34
|
+
extract_from_gradle('versionCode')&.to_i
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get version name
|
|
38
|
+
def version_name
|
|
39
|
+
extract_from_gradle('versionName')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get minimum SDK version
|
|
43
|
+
def min_sdk_version
|
|
44
|
+
extract_from_gradle('minSdk') || extract_from_gradle('minSdkVersion')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get target SDK version
|
|
48
|
+
def target_sdk_version
|
|
49
|
+
extract_from_gradle('targetSdk') || extract_from_gradle('targetSdkVersion')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get compile SDK version
|
|
53
|
+
def compile_sdk_version
|
|
54
|
+
extract_from_gradle('compileSdk') || extract_from_gradle('compileSdkVersion')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get all build types (debug, release, etc.)
|
|
58
|
+
def build_types
|
|
59
|
+
types = []
|
|
60
|
+
|
|
61
|
+
# Match buildTypes block
|
|
62
|
+
if @gradle_content =~ /buildTypes\s*\{(.*?)\n\s*\}/m
|
|
63
|
+
block = $1
|
|
64
|
+
# Find all type names (e.g., "release {" or "debug {")
|
|
65
|
+
block.scan(/(\w+)\s*\{/) do |match|
|
|
66
|
+
types << match[0] unless %w[debug release].include?(match[0]) && types.include?(match[0])
|
|
67
|
+
types << match[0]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Default build types if none found
|
|
72
|
+
types = ['debug', 'release'] if types.empty?
|
|
73
|
+
types.uniq
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get all product flavors
|
|
77
|
+
def product_flavors
|
|
78
|
+
flavors = []
|
|
79
|
+
|
|
80
|
+
if @gradle_content =~ /productFlavors\s*\{(.*?)\n\s{4}\}/m
|
|
81
|
+
block = $1
|
|
82
|
+
block.scan(/(\w+)\s*\{/) do |match|
|
|
83
|
+
flavors << match[0]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
flavors
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get signing config for a build type
|
|
91
|
+
def signing_config(build_type = 'release')
|
|
92
|
+
# Look for signingConfig in the build type block
|
|
93
|
+
if @gradle_content =~ /#{build_type}\s*\{[^}]*signingConfig\s*(?:=\s*)?signingConfigs\.(\w+)/m
|
|
94
|
+
return $1
|
|
95
|
+
end
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if signing is configured for release builds
|
|
100
|
+
def signing_configured?(build_type = 'release')
|
|
101
|
+
signing_config(build_type) != nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get signing configs defined in the project
|
|
105
|
+
def signing_configs
|
|
106
|
+
configs = []
|
|
107
|
+
|
|
108
|
+
if @gradle_content =~ /signingConfigs\s*\{(.*?)\n\s{4}\}/m
|
|
109
|
+
block = $1
|
|
110
|
+
block.scan(/(\w+)\s*\{/) do |match|
|
|
111
|
+
configs << match[0]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
configs
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get keystore path from signing config
|
|
119
|
+
def keystore_path(config_name = 'release')
|
|
120
|
+
if @gradle_content =~ /#{config_name}\s*\{[^}]*storeFile\s*(?:=\s*)?(?:file\()?"?([^")\n]+)"?\)?/m
|
|
121
|
+
return $1
|
|
122
|
+
end
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get keystore alias from signing config
|
|
127
|
+
def keystore_alias(config_name = 'release')
|
|
128
|
+
if @gradle_content =~ /#{config_name}\s*\{[^}]*keyAlias\s*(?:=\s*)?["']?([^"'\n]+)["']?/m
|
|
129
|
+
return $1.strip
|
|
130
|
+
end
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get the app name from strings.xml or manifest
|
|
135
|
+
def app_name
|
|
136
|
+
# Try strings.xml first
|
|
137
|
+
strings_path = File.join(android_directory, 'app/src/main/res/values/strings.xml')
|
|
138
|
+
if File.exist?(strings_path)
|
|
139
|
+
content = File.read(strings_path)
|
|
140
|
+
if content =~ /<string\s+name="app_name"[^>]*>([^<]+)<\/string>/
|
|
141
|
+
return $1
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Fallback to manifest label
|
|
146
|
+
if @manifest_content && @manifest_content =~ /android:label="([^"]+)"/
|
|
147
|
+
return $1
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Fallback to directory name
|
|
151
|
+
File.basename(@project_info[:directory])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check if project uses Kotlin DSL
|
|
155
|
+
def kotlin_dsl?
|
|
156
|
+
@project_info[:app_build_gradle]&.end_with?('.kts')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Get the gradle wrapper command
|
|
160
|
+
def gradle_command
|
|
161
|
+
wrapper_path = File.join(android_directory, 'gradlew')
|
|
162
|
+
File.exist?(wrapper_path) ? './gradlew' : 'gradle'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Check if gradle wrapper exists
|
|
166
|
+
def gradle_wrapper_exists?
|
|
167
|
+
File.exist?(File.join(android_directory, 'gradlew'))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Get available build variants (build type + flavor combinations)
|
|
171
|
+
def build_variants
|
|
172
|
+
flavors = product_flavors
|
|
173
|
+
types = build_types
|
|
174
|
+
|
|
175
|
+
if flavors.empty?
|
|
176
|
+
types.map { |t| t }
|
|
177
|
+
else
|
|
178
|
+
flavors.flat_map do |flavor|
|
|
179
|
+
types.map { |type| "#{flavor}#{type.capitalize}" }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Get AAB output path for a variant
|
|
185
|
+
def aab_output_path(variant = 'release')
|
|
186
|
+
# Standard output location
|
|
187
|
+
File.join(android_directory, "app/build/outputs/bundle/#{variant}/app-#{variant}.aab")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get APK output path for a variant
|
|
191
|
+
def apk_output_path(variant = 'release')
|
|
192
|
+
File.join(android_directory, "app/build/outputs/apk/#{variant}/app-#{variant}.apk")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Get Android directory
|
|
196
|
+
def android_directory
|
|
197
|
+
@project_info[:android_directory] || @project_info[:path]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Get project summary
|
|
201
|
+
def summary
|
|
202
|
+
{
|
|
203
|
+
application_id: application_id,
|
|
204
|
+
version_code: version_code,
|
|
205
|
+
version_name: version_name,
|
|
206
|
+
min_sdk: min_sdk_version,
|
|
207
|
+
target_sdk: target_sdk_version,
|
|
208
|
+
build_types: build_types,
|
|
209
|
+
product_flavors: product_flavors,
|
|
210
|
+
signing_configured: signing_configured?,
|
|
211
|
+
kotlin_dsl: kotlin_dsl?,
|
|
212
|
+
gradle_wrapper: gradle_wrapper_exists?
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def read_gradle_file
|
|
219
|
+
gradle_path = @project_info[:app_build_gradle]
|
|
220
|
+
|
|
221
|
+
unless gradle_path && File.exist?(gradle_path)
|
|
222
|
+
# Try to find it
|
|
223
|
+
android_dir = android_directory
|
|
224
|
+
gradle_path = File.join(android_dir, 'app/build.gradle')
|
|
225
|
+
gradle_path = File.join(android_dir, 'app/build.gradle.kts') unless File.exist?(gradle_path)
|
|
226
|
+
gradle_path = File.join(android_dir, 'build.gradle') unless File.exist?(gradle_path)
|
|
227
|
+
gradle_path = File.join(android_dir, 'build.gradle.kts') unless File.exist?(gradle_path)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
return '' unless File.exist?(gradle_path)
|
|
231
|
+
File.read(gradle_path)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def read_manifest_file
|
|
235
|
+
manifest_path = File.join(android_directory, 'app/src/main/AndroidManifest.xml')
|
|
236
|
+
return nil unless File.exist?(manifest_path)
|
|
237
|
+
File.read(manifest_path)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def extract_from_gradle(property)
|
|
241
|
+
# Handle both Groovy and Kotlin DSL syntax
|
|
242
|
+
# Groovy: applicationId "com.example.app" or applicationId = "com.example.app"
|
|
243
|
+
# Kotlin: applicationId = "com.example.app"
|
|
244
|
+
|
|
245
|
+
patterns = [
|
|
246
|
+
/#{property}\s*=?\s*["']([^"']+)["']/,
|
|
247
|
+
/#{property}\s+["']([^"']+)["']/,
|
|
248
|
+
/#{property}\s*=\s*(\d+)/,
|
|
249
|
+
/#{property}\s+(\d+)/
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
patterns.each do |pattern|
|
|
253
|
+
if @gradle_content =~ pattern
|
|
254
|
+
return $1
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
nil
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def extract_package_from_manifest
|
|
262
|
+
return nil unless @manifest_content
|
|
263
|
+
|
|
264
|
+
if @manifest_content =~ /package="([^"]+)"/
|
|
265
|
+
return $1
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def extract_from_expo_config
|
|
272
|
+
# Check for app.json in project root
|
|
273
|
+
project_dir = @project_info[:directory]
|
|
274
|
+
app_json_path = File.join(project_dir, 'app.json')
|
|
275
|
+
|
|
276
|
+
return nil unless File.exist?(app_json_path)
|
|
277
|
+
|
|
278
|
+
begin
|
|
279
|
+
require 'json'
|
|
280
|
+
config = JSON.parse(File.read(app_json_path))
|
|
281
|
+
|
|
282
|
+
# Expo config can be nested under 'expo' key or at root
|
|
283
|
+
expo_config = config['expo'] || config
|
|
284
|
+
|
|
285
|
+
# Get Android package name
|
|
286
|
+
expo_config.dig('android', 'package')
|
|
287
|
+
rescue
|
|
288
|
+
nil
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|