Skip to content

Commit f39269f

Browse files
authored
Validate apps for Build Distribution (#43)
1 parent 81058fa commit f39269f

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed

emerge_cli.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
3636
spec.add_dependency 'open3', '~> 0.2.1'
3737
spec.add_dependency 'ruby-macho', '~> 4.1.0'
3838
spec.add_dependency 'ruby_tree_sitter', '~> 1.9'
39+
spec.add_dependency 'rubyzip', '~> 2.3.0'
3940
spec.add_dependency 'tty-prompt', '~> 0.23.1'
4041
spec.add_dependency 'tty-table', '~> 0.12.0'
4142
spec.add_dependency 'xcodeproj', '~> 1.27.0'
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
require 'dry/cli'
2+
require 'cfpropertylist'
3+
require 'zip'
4+
require 'rbconfig'
5+
6+
module EmergeCLI
7+
module Commands
8+
module BuildDistribution
9+
class ValidateApp < EmergeCLI::Commands::GlobalOptions
10+
desc 'Validate app for build distribution'
11+
12+
option :path, type: :string, required: true, desc: 'Path to the xcarchive, IPA or APK to validate'
13+
14+
# Constants
15+
PLIST_START = '<plist'.freeze
16+
PLIST_STOP = '</plist>'.freeze
17+
18+
UTF8_ENCODING = 'UTF-8'.freeze
19+
STRING_FORMAT = 'binary'.freeze
20+
EMPTY_STRING = ''.freeze
21+
22+
EXPECTED_ABI = 'arm64-v8a'.freeze
23+
24+
def call(**options)
25+
@options = options
26+
before(options)
27+
28+
Sync do
29+
file_extension = File.extname(@options[:path])
30+
case file_extension
31+
when '.xcarchive'
32+
handle_xcarchive
33+
when '.ipa'
34+
handle_ipa
35+
when '.app'
36+
handle_app
37+
when '.apk'
38+
handle_apk
39+
else
40+
raise "Unknown file extension: #{file_extension}"
41+
end
42+
end
43+
end
44+
45+
private
46+
47+
def handle_xcarchive
48+
raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
49+
50+
app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
51+
run_codesign_check(app_path)
52+
read_provisioning_profile(app_path)
53+
end
54+
55+
def handle_ipa
56+
raise 'Path must be an IPA' unless @options[:path].end_with?('.ipa')
57+
58+
Dir.mktmpdir do |tmp_dir|
59+
Zip::File.open(@options[:path]) do |zip_file|
60+
zip_file.each do |entry|
61+
entry.extract(File.join(tmp_dir, entry.name))
62+
end
63+
end
64+
65+
app_path = File.join(tmp_dir, 'Payload/*.app')
66+
app_path = Dir.glob(app_path).first
67+
run_codesign_check(app_path)
68+
read_provisioning_profile(app_path)
69+
end
70+
end
71+
72+
def handle_app
73+
raise 'Path must be an app' unless @options[:path].end_with?('.app')
74+
75+
app_path = @options[:path]
76+
run_codesign_check(app_path)
77+
read_provisioning_profile(app_path)
78+
end
79+
80+
def handle_apk
81+
raise 'Path must be an APK' unless @options[:path].end_with?('.apk')
82+
83+
apk_path = @options[:path]
84+
check_supported_abis(apk_path)
85+
end
86+
87+
def run_codesign_check(app_path)
88+
unless RbConfig::CONFIG['host_os'] =~ /darwin/i
89+
Logger.info 'Skipping codesign check on non-macOS platform'
90+
return
91+
end
92+
93+
command = "codesign -dvvv '#{app_path}'"
94+
Logger.debug command
95+
stdout, _, status = Open3.capture3(command)
96+
Logger.debug stdout
97+
raise '❌ Codesign check failed' unless status.success?
98+
99+
Logger.info '✅ Codesign check passed'
100+
end
101+
102+
def read_provisioning_profile(app_path)
103+
entitlements_path = File.join(app_path, 'embedded.mobileprovision')
104+
raise '❌ Entitlements file not found' unless File.exist?(entitlements_path)
105+
106+
content = File.read(entitlements_path)
107+
lines = content.lines
108+
109+
buffer = ''
110+
inside_plist = false
111+
lines.each do |line|
112+
inside_plist = true if line.include? PLIST_START
113+
if inside_plist
114+
buffer << line
115+
break if line.include? PLIST_STOP
116+
end
117+
end
118+
119+
encoded_plist = buffer.encode(UTF8_ENCODING, STRING_FORMAT, invalid: :replace, undef: :replace,
120+
replace: EMPTY_STRING)
121+
encoded_plist = encoded_plist.sub(/#{PLIST_STOP}.+/, PLIST_STOP)
122+
123+
plist = CFPropertyList::List.new(data: encoded_plist)
124+
parsed_data = CFPropertyList.native_types(plist.value)
125+
126+
expiration_date = parsed_data['ExpirationDate']
127+
if expiration_date > Time.now
128+
Logger.info '✅ Provisioning profile hasn\'t expired'
129+
else
130+
Logger.info "❌ Provisioning profile is expired. Expiration date: #{expiration_date}"
131+
end
132+
133+
provisions_all_devices = parsed_data['ProvisionsAllDevices']
134+
if provisions_all_devices
135+
Logger.info 'Provisioning profile supports all devices (likely an enterprise profile)'
136+
else
137+
devices = parsed_data['ProvisionedDevices']
138+
Logger.info 'Provisioning profile does not support all devices (likely a development profile).'
139+
Logger.info "Devices: #{devices.inspect}"
140+
end
141+
end
142+
143+
def check_supported_abis(apk_path)
144+
abis = []
145+
146+
Zip::File.open(apk_path) do |zip_file|
147+
zip_file.each do |entry|
148+
if entry.name.start_with?('lib/') && entry.name.count('/') == 2
149+
abi = entry.name.split('/')[1]
150+
abis << abi unless abis.include?(abi)
151+
end
152+
end
153+
end
154+
155+
unless abis.include?(EXPECTED_ABI)
156+
raise "APK does not support #{EXPECTED_ABI} architecture, found: #{abis.join(', ')}"
157+
end
158+
159+
Logger.info "✅ APK supports #{EXPECTED_ABI} architecture"
160+
end
161+
end
162+
end
163+
end
164+
end

lib/emerge_cli.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
require_relative 'commands/order_files/validate_linkmaps'
1616
require_relative 'commands/order_files/validate_xcode_project'
1717
require_relative 'commands/upload/build'
18+
require_relative 'commands/build_distribution/validate_app'
1819

1920
require_relative 'reaper/ast_parser'
2021
require_relative 'reaper/code_deleter'
@@ -61,6 +62,10 @@ module EmergeCLI
6162
prefix.register 'validate-linkmaps', Commands::ValidateLinkmaps
6263
prefix.register 'validate-xcode-project', Commands::ValidateXcodeProject
6364
end
65+
66+
register 'build-distribution' do |prefix|
67+
prefix.register 'validate-app', Commands::BuildDistribution::ValidateApp
68+
end
6469
end
6570

6671
# By default the log level is INFO, but can be overridden by the --debug flag

0 commit comments

Comments
 (0)