emerge 0.6.2 → 0.7.1
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/commands/build/distribution/install.rb +186 -0
- data/lib/commands/build/distribution/validate.rb +166 -0
- data/lib/commands/{autofixes → fix}/exported_symbols.rb +1 -1
- data/lib/commands/{autofixes → fix}/minify_strings.rb +1 -1
- data/lib/commands/{autofixes → fix}/strip_binary_symbols.rb +1 -1
- data/lib/commands/order_files/download_order_files.rb +51 -49
- data/lib/commands/order_files/validate_linkmaps.rb +34 -32
- data/lib/commands/order_files/validate_xcode_project.rb +46 -44
- data/lib/commands/reaper/reaper.rb +2 -2
- data/lib/commands/upload/snapshots/snapshots.rb +84 -0
- data/lib/emerge_cli.rb +37 -36
- data/lib/version.rb +1 -1
- metadata +7 -7
- data/lib/commands/build_distribution/download_and_install.rb +0 -139
- data/lib/commands/build_distribution/validate_app.rb +0 -164
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 409fec8581e499747b3c9ca5cd80e0f9c9e491ca0d94688bc1e0d299a974a95b
|
4
|
+
data.tar.gz: 64863c523c84b25f5ff21a9bbda0863f13ef7a5165d5437aeb66d60211e7fe84
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a6dc2be9e2ad1f3078c9f6703d35c2f1e70d5630d14a2f682216fb949c1111b7bc236702e2e9698714f4662e5e7a70bcb67118c510542b61d1b58c00f13dc1e
|
7
|
+
data.tar.gz: 76f622ef6797a7c420d1406e9f43ddceb9f075180641ae6e5b78cbb63a3a18489d883d63346d973f9ff8e6b225281d2d0671e6ad33d5ff667b5c7d347c8ba769
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'dry/cli'
|
2
|
+
require 'cfpropertylist'
|
3
|
+
require 'zip'
|
4
|
+
require 'rbconfig'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'tty-prompt'
|
7
|
+
|
8
|
+
module EmergeCLI
|
9
|
+
module Commands
|
10
|
+
module Build
|
11
|
+
module Distribution
|
12
|
+
class Install < EmergeCLI::Commands::GlobalOptions
|
13
|
+
desc 'Download and install a build from Build Distribution'
|
14
|
+
|
15
|
+
option :api_token, type: :string, required: false,
|
16
|
+
desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
|
17
|
+
option :id, type: :string, required: true, desc: 'Emerge build ID to download'
|
18
|
+
option :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
|
19
|
+
option :device_id, type: :string, desc: 'Specific device ID to target'
|
20
|
+
option :device_type, type: :string, enum: %w[virtual physical any], default: 'any',
|
21
|
+
desc: 'Type of device to target (virtual/physical/any)'
|
22
|
+
option :output, type: :string, required: false, desc: 'Output path for the downloaded build'
|
23
|
+
|
24
|
+
def initialize(network: nil)
|
25
|
+
@network = network
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(**options)
|
29
|
+
@options = options
|
30
|
+
before(options)
|
31
|
+
|
32
|
+
Sync do
|
33
|
+
api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
|
34
|
+
raise 'API token is required' unless api_token
|
35
|
+
|
36
|
+
raise 'Build ID is required' unless @options[:id]
|
37
|
+
|
38
|
+
output_name = nil
|
39
|
+
app_id = nil
|
40
|
+
|
41
|
+
begin
|
42
|
+
@network ||= EmergeCLI::Network.new(api_token:)
|
43
|
+
|
44
|
+
Logger.info 'Getting build URL...'
|
45
|
+
request = get_build_url(@options[:id])
|
46
|
+
response = parse_response(request)
|
47
|
+
|
48
|
+
platform = response['platform']
|
49
|
+
download_url = response['downloadUrl']
|
50
|
+
app_id = response['appId']
|
51
|
+
|
52
|
+
extension = platform == 'ios' ? 'ipa' : 'apk'
|
53
|
+
output_name = @options[:output] || "#{@options[:id]}.#{extension}"
|
54
|
+
|
55
|
+
if File.exist?(output_name)
|
56
|
+
Logger.info "Build file already exists at #{output_name}"
|
57
|
+
prompt = TTY::Prompt.new
|
58
|
+
choice = prompt.select('What would you like to do?', {
|
59
|
+
'Install existing file' => :install,
|
60
|
+
'Overwrite with new download' => :overwrite,
|
61
|
+
'Cancel' => :cancel
|
62
|
+
})
|
63
|
+
|
64
|
+
case choice
|
65
|
+
when :install
|
66
|
+
Logger.info 'Proceeding with existing file...'
|
67
|
+
when :overwrite
|
68
|
+
Logger.info 'Downloading new build...'
|
69
|
+
`curl --progress-bar -L '#{download_url}' -o #{output_name}`
|
70
|
+
Logger.info "✅ Build downloaded to #{output_name}"
|
71
|
+
when :cancel
|
72
|
+
raise 'Operation cancelled by user'
|
73
|
+
end
|
74
|
+
else
|
75
|
+
Logger.info 'Downloading build...'
|
76
|
+
`curl --progress-bar -L '#{download_url}' -o #{output_name}`
|
77
|
+
Logger.info "✅ Build downloaded to #{output_name}"
|
78
|
+
end
|
79
|
+
rescue StandardError => e
|
80
|
+
Logger.error "❌ Failed to download build: #{e.message}"
|
81
|
+
raise e
|
82
|
+
ensure
|
83
|
+
@network&.close
|
84
|
+
end
|
85
|
+
|
86
|
+
begin
|
87
|
+
if @options[:install] && !output_name.nil?
|
88
|
+
if platform == 'ios'
|
89
|
+
install_ios_build(output_name, app_id)
|
90
|
+
elsif platform == 'android'
|
91
|
+
install_android_build(output_name)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
rescue StandardError => e
|
95
|
+
Logger.error "❌ Failed to install build: #{e.message}"
|
96
|
+
raise e
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def get_build_url(build_id)
|
104
|
+
@network.get(
|
105
|
+
path: '/distribution/downloadUrl',
|
106
|
+
max_retries: 3,
|
107
|
+
query: {
|
108
|
+
buildId: build_id
|
109
|
+
}
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_response(response)
|
114
|
+
case response.status
|
115
|
+
when 200
|
116
|
+
JSON.parse(response.read)
|
117
|
+
when 400
|
118
|
+
error_message = JSON.parse(response.read)['errorMessage']
|
119
|
+
raise "Invalid parameters: #{error_message}"
|
120
|
+
when 401, 403
|
121
|
+
raise 'Invalid API token'
|
122
|
+
else
|
123
|
+
raise "Getting build failed with status #{response.status}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def install_ios_build(build_path, app_id)
|
128
|
+
device_type = case @options[:device_type]
|
129
|
+
when 'simulator'
|
130
|
+
XcodeDeviceManager::DeviceType::VIRTUAL
|
131
|
+
when 'physical'
|
132
|
+
XcodeDeviceManager::DeviceType::PHYSICAL
|
133
|
+
else
|
134
|
+
XcodeDeviceManager::DeviceType::ANY
|
135
|
+
end
|
136
|
+
|
137
|
+
device_manager = XcodeDeviceManager.new
|
138
|
+
device = if @options[:device_id]
|
139
|
+
device_manager.find_device_by_id(@options[:device_id])
|
140
|
+
else
|
141
|
+
device_manager.find_device_by_type(device_type, build_path)
|
142
|
+
end
|
143
|
+
|
144
|
+
Logger.info "Installing build on #{device.device_id}"
|
145
|
+
device.install_app(build_path)
|
146
|
+
Logger.info '✅ Build installed'
|
147
|
+
|
148
|
+
Logger.info "Launching app #{app_id}..."
|
149
|
+
device.launch_app(app_id)
|
150
|
+
Logger.info '✅ Build launched'
|
151
|
+
end
|
152
|
+
|
153
|
+
def install_android_build(build_path)
|
154
|
+
device_id = @options[:device_id] || select_android_device
|
155
|
+
raise 'No Android devices found' unless device_id
|
156
|
+
|
157
|
+
command = "adb -s #{device_id} install #{build_path}"
|
158
|
+
Logger.debug "Running command: #{command}"
|
159
|
+
`#{command}`
|
160
|
+
|
161
|
+
Logger.info '✅ Build installed'
|
162
|
+
end
|
163
|
+
|
164
|
+
def select_android_device
|
165
|
+
devices = get_android_devices
|
166
|
+
return nil if devices.empty?
|
167
|
+
return devices.first if devices.length == 1
|
168
|
+
|
169
|
+
prompt = TTY::Prompt.new
|
170
|
+
Logger.info 'Multiple Android devices found.'
|
171
|
+
prompt.select('Choose a device:', devices)
|
172
|
+
end
|
173
|
+
|
174
|
+
def get_android_devices
|
175
|
+
output = `adb devices`
|
176
|
+
# Split output into lines, remove first line (header), and extract device IDs
|
177
|
+
output.split("\n")[1..]
|
178
|
+
.map(&:strip)
|
179
|
+
.reject(&:empty?)
|
180
|
+
.map { |line| line.split("\t").first }
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'dry/cli'
|
2
|
+
require 'cfpropertylist'
|
3
|
+
require 'zip'
|
4
|
+
require 'rbconfig'
|
5
|
+
|
6
|
+
module EmergeCLI
|
7
|
+
module Commands
|
8
|
+
module Build
|
9
|
+
module Distribution
|
10
|
+
class ValidateApp < EmergeCLI::Commands::GlobalOptions
|
11
|
+
desc 'Validate app for build distribution'
|
12
|
+
|
13
|
+
option :path, type: :string, required: true, desc: 'Path to the xcarchive, IPA or APK to validate'
|
14
|
+
|
15
|
+
# Constants
|
16
|
+
PLIST_START = '<plist'.freeze
|
17
|
+
PLIST_STOP = '</plist>'.freeze
|
18
|
+
|
19
|
+
UTF8_ENCODING = 'UTF-8'.freeze
|
20
|
+
STRING_FORMAT = 'binary'.freeze
|
21
|
+
EMPTY_STRING = ''.freeze
|
22
|
+
|
23
|
+
EXPECTED_ABI = 'arm64-v8a'.freeze
|
24
|
+
|
25
|
+
def call(**options)
|
26
|
+
@options = options
|
27
|
+
before(options)
|
28
|
+
|
29
|
+
Sync do
|
30
|
+
file_extension = File.extname(@options[:path])
|
31
|
+
case file_extension
|
32
|
+
when '.xcarchive'
|
33
|
+
handle_xcarchive
|
34
|
+
when '.ipa'
|
35
|
+
handle_ipa
|
36
|
+
when '.app'
|
37
|
+
handle_app
|
38
|
+
when '.apk'
|
39
|
+
handle_apk
|
40
|
+
else
|
41
|
+
raise "Unknown file extension: #{file_extension}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def handle_xcarchive
|
49
|
+
raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
|
50
|
+
|
51
|
+
app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
|
52
|
+
run_codesign_check(app_path)
|
53
|
+
read_provisioning_profile(app_path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_ipa
|
57
|
+
raise 'Path must be an IPA' unless @options[:path].end_with?('.ipa')
|
58
|
+
|
59
|
+
Dir.mktmpdir do |tmp_dir|
|
60
|
+
Zip::File.open(@options[:path]) do |zip_file|
|
61
|
+
zip_file.each do |entry|
|
62
|
+
entry.extract(File.join(tmp_dir, entry.name))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
app_path = File.join(tmp_dir, 'Payload/*.app')
|
67
|
+
app_path = Dir.glob(app_path).first
|
68
|
+
run_codesign_check(app_path)
|
69
|
+
read_provisioning_profile(app_path)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def handle_app
|
74
|
+
raise 'Path must be an app' unless @options[:path].end_with?('.app')
|
75
|
+
|
76
|
+
app_path = @options[:path]
|
77
|
+
run_codesign_check(app_path)
|
78
|
+
read_provisioning_profile(app_path)
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle_apk
|
82
|
+
raise 'Path must be an APK' unless @options[:path].end_with?('.apk')
|
83
|
+
|
84
|
+
apk_path = @options[:path]
|
85
|
+
check_supported_abis(apk_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
def run_codesign_check(app_path)
|
89
|
+
unless RbConfig::CONFIG['host_os'] =~ /darwin/i
|
90
|
+
Logger.info 'Skipping codesign check on non-macOS platform'
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
command = "codesign -dvvv '#{app_path}'"
|
95
|
+
Logger.debug command
|
96
|
+
stdout, _, status = Open3.capture3(command)
|
97
|
+
Logger.debug stdout
|
98
|
+
raise '❌ Codesign check failed' unless status.success?
|
99
|
+
|
100
|
+
Logger.info '✅ Codesign check passed'
|
101
|
+
end
|
102
|
+
|
103
|
+
def read_provisioning_profile(app_path)
|
104
|
+
entitlements_path = File.join(app_path, 'embedded.mobileprovision')
|
105
|
+
raise '❌ Entitlements file not found' unless File.exist?(entitlements_path)
|
106
|
+
|
107
|
+
content = File.read(entitlements_path)
|
108
|
+
lines = content.lines
|
109
|
+
|
110
|
+
buffer = ''
|
111
|
+
inside_plist = false
|
112
|
+
lines.each do |line|
|
113
|
+
inside_plist = true if line.include? PLIST_START
|
114
|
+
if inside_plist
|
115
|
+
buffer << line
|
116
|
+
break if line.include? PLIST_STOP
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
encoded_plist = buffer.encode(UTF8_ENCODING, STRING_FORMAT, invalid: :replace, undef: :replace,
|
121
|
+
replace: EMPTY_STRING)
|
122
|
+
encoded_plist = encoded_plist.sub(/#{PLIST_STOP}.+/, PLIST_STOP)
|
123
|
+
|
124
|
+
plist = CFPropertyList::List.new(data: encoded_plist)
|
125
|
+
parsed_data = CFPropertyList.native_types(plist.value)
|
126
|
+
|
127
|
+
expiration_date = parsed_data['ExpirationDate']
|
128
|
+
if expiration_date > Time.now
|
129
|
+
Logger.info '✅ Provisioning profile hasn\'t expired'
|
130
|
+
else
|
131
|
+
Logger.info "❌ Provisioning profile is expired. Expiration date: #{expiration_date}"
|
132
|
+
end
|
133
|
+
|
134
|
+
provisions_all_devices = parsed_data['ProvisionsAllDevices']
|
135
|
+
if provisions_all_devices
|
136
|
+
Logger.info 'Provisioning profile supports all devices (likely an enterprise profile)'
|
137
|
+
else
|
138
|
+
devices = parsed_data['ProvisionedDevices']
|
139
|
+
Logger.info 'Provisioning profile does not support all devices (likely a development profile).'
|
140
|
+
Logger.info "Devices: #{devices.inspect}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def check_supported_abis(apk_path)
|
145
|
+
abis = []
|
146
|
+
|
147
|
+
Zip::File.open(apk_path) do |zip_file|
|
148
|
+
zip_file.each do |entry|
|
149
|
+
if entry.name.start_with?('lib/') && entry.name.count('/') == 2
|
150
|
+
abi = entry.name.split('/')[1]
|
151
|
+
abis << abi unless abis.include?(abi)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
unless abis.include?(EXPECTED_ABI)
|
157
|
+
raise "APK does not support #{EXPECTED_ABI} architecture, found: #{abis.join(', ')}"
|
158
|
+
end
|
159
|
+
|
160
|
+
Logger.info "✅ APK supports #{EXPECTED_ABI} architecture"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -2,75 +2,77 @@ require 'dry/cli'
|
|
2
2
|
|
3
3
|
module EmergeCLI
|
4
4
|
module Commands
|
5
|
-
|
6
|
-
|
5
|
+
module OrderFiles
|
6
|
+
class Download < EmergeCLI::Commands::GlobalOptions
|
7
|
+
desc 'Download order files from Emerge'
|
7
8
|
|
8
|
-
|
9
|
+
option :bundle_id, type: :string, required: true, desc: 'Bundle identifier to download order files for'
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
option :api_token, type: :string, required: false,
|
12
|
+
desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
|
12
13
|
|
13
|
-
|
14
|
-
|
14
|
+
option :app_version, type: :string, required: true,
|
15
|
+
desc: 'App version to download order files for'
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
option :unzip, type: :boolean, required: false,
|
18
|
+
desc: 'Unzip the order file after downloading'
|
18
19
|
|
19
|
-
|
20
|
-
|
20
|
+
option :output, type: :string, required: false,
|
21
|
+
desc: 'Output name for the order file, defaults to bundle_id-app_version.gz'
|
21
22
|
|
22
|
-
|
23
|
+
EMERGE_ORDER_FILE_URL = 'order-files-prod.emergetools.com'.freeze
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
def initialize(network: nil)
|
26
|
+
@network = network
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def call(**options)
|
30
|
+
@options = options
|
31
|
+
before(options)
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
begin
|
34
|
+
api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
|
35
|
+
raise 'API token is required' unless api_token
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
raise 'Bundle ID is required' unless @options[:bundle_id]
|
38
|
+
raise 'App version is required' unless @options[:app_version]
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
@network ||= EmergeCLI::Network.new(api_token:, base_url: EMERGE_ORDER_FILE_URL)
|
41
|
+
output_name = @options[:output] || "#{@options[:bundle_id]}-#{@options[:app_version]}.gz"
|
42
|
+
output_name = "#{output_name}.gz" unless output_name.end_with?('.gz')
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
Sync do
|
45
|
+
request = get_order_file(options[:bundle_id], options[:app_version])
|
46
|
+
response = request.read
|
46
47
|
|
47
|
-
|
48
|
+
File.write(output_name, response)
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
if @options[:unzip]
|
51
|
+
Logger.info 'Unzipping order file...'
|
52
|
+
Zlib::GzipReader.open(output_name) do |gz|
|
53
|
+
File.write(output_name.gsub('.gz', ''), gz.read)
|
54
|
+
end
|
53
55
|
end
|
54
|
-
end
|
55
56
|
|
56
|
-
|
57
|
+
Logger.info 'Order file downloaded successfully'
|
58
|
+
end
|
59
|
+
rescue StandardError => e
|
60
|
+
Logger.error "Failed to download order file: #{e.message}"
|
61
|
+
Logger.error 'Check your parameters and try again'
|
62
|
+
raise e
|
63
|
+
ensure
|
64
|
+
@network&.close
|
57
65
|
end
|
58
|
-
rescue StandardError => e
|
59
|
-
Logger.error "Failed to download order file: #{e.message}"
|
60
|
-
Logger.error 'Check your parameters and try again'
|
61
|
-
raise e
|
62
|
-
ensure
|
63
|
-
@network&.close
|
64
66
|
end
|
65
|
-
end
|
66
67
|
|
67
|
-
|
68
|
+
private
|
68
69
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
70
|
+
def get_order_file(bundle_id, app_version)
|
71
|
+
@network.get(
|
72
|
+
path: "/#{bundle_id}/#{app_version}",
|
73
|
+
max_retries: 0
|
74
|
+
)
|
75
|
+
end
|
74
76
|
end
|
75
77
|
end
|
76
78
|
end
|
@@ -3,52 +3,54 @@ require 'cfpropertylist'
|
|
3
3
|
|
4
4
|
module EmergeCLI
|
5
5
|
module Commands
|
6
|
-
|
7
|
-
|
6
|
+
module OrderFiles
|
7
|
+
class ValidateLinkmaps < EmergeCLI::Commands::GlobalOptions
|
8
|
+
desc 'Validate linkmaps in xcarchive'
|
8
9
|
|
9
|
-
|
10
|
+
option :path, type: :string, required: true, desc: 'Path to the xcarchive to validate'
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
def initialize(network: nil)
|
13
|
+
@network = network
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
def call(**options)
|
17
|
+
@options = options
|
18
|
+
before(options)
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
Sync do
|
21
|
+
executable_name = get_executable_name
|
22
|
+
raise 'Executable not found' if executable_name.nil?
|
22
23
|
|
23
|
-
|
24
|
+
Logger.info "Using executable: #{executable_name}"
|
24
25
|
|
25
|
-
|
26
|
-
|
26
|
+
linkmaps_path = File.join(@options[:path], 'Linkmaps')
|
27
|
+
raise 'Linkmaps folder not found' unless File.directory?(linkmaps_path)
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
linkmaps = Dir.glob("#{linkmaps_path}/*.txt")
|
30
|
+
raise 'No linkmaps found' if linkmaps.empty?
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
executable_linkmaps = linkmaps.select do |linkmap|
|
33
|
+
File.basename(linkmap).start_with?(executable_name)
|
34
|
+
end
|
35
|
+
raise 'No linkmaps found for executable' if executable_linkmaps.empty?
|
35
36
|
|
36
|
-
|
37
|
+
Logger.info "✅ Found linkmaps for #{executable_name}"
|
38
|
+
end
|
37
39
|
end
|
38
|
-
end
|
39
40
|
|
40
|
-
|
41
|
+
private
|
41
42
|
|
42
|
-
|
43
|
-
|
43
|
+
def get_executable_name
|
44
|
+
raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
|
47
|
+
info_path = File.join(app_path, 'Info.plist')
|
48
|
+
plist_data = File.read(info_path)
|
49
|
+
plist = CFPropertyList::List.new(data: plist_data)
|
50
|
+
parsed_data = CFPropertyList.native_types(plist.value)
|
50
51
|
|
51
|
-
|
52
|
+
parsed_data['CFBundleExecutable']
|
53
|
+
end
|
52
54
|
end
|
53
55
|
end
|
54
56
|
end
|
@@ -3,67 +3,69 @@ require 'xcodeproj'
|
|
3
3
|
|
4
4
|
module EmergeCLI
|
5
5
|
module Commands
|
6
|
-
|
7
|
-
|
6
|
+
module OrderFiles
|
7
|
+
class ValidateXcodeProject < EmergeCLI::Commands::GlobalOptions
|
8
|
+
desc 'Validate xcodeproject for order files'
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
option :path, type: :string, required: true, desc: 'Path to the xcodeproject to validate'
|
11
|
+
option :target, type: :string, required: false, desc: 'Target to validate'
|
12
|
+
option :build_configuration, type: :string, required: false,
|
13
|
+
desc: 'Build configuration to validate (Release by default)'
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
# Constants
|
16
|
+
LINK_MAPS_CONFIG = 'LD_GENERATE_MAP_FILE'.freeze
|
17
|
+
LINK_MAPS_PATH = 'LD_MAP_FILE_PATH'.freeze
|
18
|
+
PATH_TO_LINKMAP = '$(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt'.freeze
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
def call(**options)
|
21
|
+
@options = options
|
22
|
+
before(options)
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
raise 'Path must be an xcodeproject' unless @options[:path].end_with?('.xcodeproj')
|
25
|
+
raise 'Path does not exist' unless File.exist?(@options[:path])
|
25
26
|
|
26
|
-
|
27
|
+
@options[:build_configuration] ||= 'Release'
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
Sync do
|
30
|
+
project = Xcodeproj::Project.open(@options[:path])
|
30
31
|
|
31
|
-
|
32
|
+
validate_xcproj(project)
|
33
|
+
end
|
32
34
|
end
|
33
|
-
end
|
34
35
|
|
35
|
-
|
36
|
+
private
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
def validate_xcproj(project)
|
39
|
+
project.targets.each do |target|
|
40
|
+
next if @options[:target] && target.name != @options[:target]
|
41
|
+
next unless target.product_type == 'com.apple.product-type.application'
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
target.build_configurations.each do |config|
|
44
|
+
next if config.name != @options[:build_configuration]
|
45
|
+
validate_target_config(target, config)
|
46
|
+
end
|
45
47
|
end
|
46
48
|
end
|
47
|
-
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
50
|
+
def validate_target_config(target, config)
|
51
|
+
has_error = false
|
52
|
+
if config.build_settings[LINK_MAPS_CONFIG] != 'YES'
|
53
|
+
has_error = true
|
54
|
+
Logger.error "❌ Write Link Map File (#{LINK_MAPS_CONFIG}) is not set to YES"
|
55
|
+
end
|
56
|
+
if config.build_settings[LINK_MAPS_PATH] != ''
|
57
|
+
has_error = true
|
58
|
+
Logger.error "❌ Path to Link Map File (#{LINK_MAPS_PATH}) is not set, we recommend \
|
58
59
|
setting it to '#{PATH_TO_LINKMAP}'"
|
59
|
-
|
60
|
+
end
|
60
61
|
|
61
|
-
|
62
|
-
|
62
|
+
if has_error
|
63
|
+
Logger.error "❌ Target '#{target.name}' has errors, this means \
|
63
64
|
that the linkmaps will not be generated as expected"
|
64
|
-
|
65
|
-
|
66
|
-
|
65
|
+
Logger.error "Use `emerge configure order-files-ios --project-path '#{@options[:path]}'` to fix this"
|
66
|
+
else
|
67
|
+
Logger.info "✅ Target '#{target.name}' is valid"
|
68
|
+
end
|
67
69
|
end
|
68
70
|
end
|
69
71
|
end
|
@@ -7,7 +7,7 @@ module EmergeCLI
|
|
7
7
|
class Reaper < EmergeCLI::Commands::GlobalOptions
|
8
8
|
desc 'Analyze dead code from an Emerge upload'
|
9
9
|
|
10
|
-
option :
|
10
|
+
option :id, type: :string, required: true, desc: 'Emerge build ID to analyze'
|
11
11
|
option :project_root, type: :string, required: true,
|
12
12
|
desc: 'Root directory of the project, defaults to current directory'
|
13
13
|
|
@@ -38,7 +38,7 @@ module EmergeCLI
|
|
38
38
|
project_root = @options[:project_root] || Dir.pwd
|
39
39
|
|
40
40
|
Sync do
|
41
|
-
all_data = @profiler.measure('fetch_dead_code') { fetch_all_dead_code(@options[:
|
41
|
+
all_data = @profiler.measure('fetch_dead_code') { fetch_all_dead_code(@options[:id]) }
|
42
42
|
result = @profiler.measure('parse_dead_code') { DeadCodeResult.new(all_data) }
|
43
43
|
|
44
44
|
Logger.info result.to_s
|
@@ -6,6 +6,8 @@ require 'async'
|
|
6
6
|
require 'async/barrier'
|
7
7
|
require 'async/semaphore'
|
8
8
|
require 'async/http/internet/instance'
|
9
|
+
require 'zip'
|
10
|
+
require 'tempfile'
|
9
11
|
|
10
12
|
module EmergeCLI
|
11
13
|
module Commands
|
@@ -33,6 +35,8 @@ module EmergeCLI
|
|
33
35
|
|
34
36
|
option :profile, type: :boolean, default: false, desc: 'Enable performance profiling metrics'
|
35
37
|
|
38
|
+
option :batch, type: :boolean, default: false, desc: 'Upload images in batch using zip file'
|
39
|
+
|
36
40
|
argument :image_paths, type: :array, required: false, desc: 'Paths to folders containing images'
|
37
41
|
|
38
42
|
def initialize(network: nil, git_info_provider: nil)
|
@@ -178,6 +182,86 @@ module EmergeCLI
|
|
178
182
|
def upload_images(run_id, concurrency, image_files, client)
|
179
183
|
Logger.info 'Uploading images...'
|
180
184
|
|
185
|
+
if @options[:batch]
|
186
|
+
batch_upload_images(run_id, image_files, client)
|
187
|
+
else
|
188
|
+
individual_upload_images(run_id, concurrency, image_files, client)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def batch_upload_images(run_id, image_files, client)
|
193
|
+
Logger.info 'Preparing batch upload...'
|
194
|
+
|
195
|
+
metadata_barrier = Async::Barrier.new
|
196
|
+
metadata_semaphore = Async::Semaphore.new(10, parent: metadata_barrier)
|
197
|
+
|
198
|
+
image_metadata = {
|
199
|
+
manifestVersion: 1,
|
200
|
+
images: {},
|
201
|
+
errors: []
|
202
|
+
}
|
203
|
+
|
204
|
+
@profiler.measure('process_image_metadata') do
|
205
|
+
image_files.each do |image_path|
|
206
|
+
metadata_semaphore.async do
|
207
|
+
file_info = client.parse_file_info(image_path)
|
208
|
+
|
209
|
+
dimensions = @profiler.measure('chunky_png_processing') do
|
210
|
+
datastream = ChunkyPNG::Datastream.from_file(image_path)
|
211
|
+
{
|
212
|
+
width: datastream.header_chunk.width,
|
213
|
+
height: datastream.header_chunk.height
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
metadata = {
|
218
|
+
fileName: file_info[:file_name],
|
219
|
+
groupName: file_info[:group_name],
|
220
|
+
displayName: file_info[:variant_name],
|
221
|
+
width: dimensions[:width],
|
222
|
+
height: dimensions[:height]
|
223
|
+
}
|
224
|
+
|
225
|
+
image_name = File.basename(image_path, '.*')
|
226
|
+
image_metadata[:images][image_name] = metadata
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
metadata_barrier.wait
|
231
|
+
end
|
232
|
+
|
233
|
+
Tempfile.create(['snapshot_batch', '.zip']) do |zip_file|
|
234
|
+
@profiler.measure('create_zip_file') do
|
235
|
+
Zip::File.open(zip_file.path, Zip::File::CREATE) do |zipfile|
|
236
|
+
zipfile.get_output_stream('manifest.json') { |f| f.write(JSON.generate(image_metadata)) }
|
237
|
+
|
238
|
+
image_files.each do |image_path|
|
239
|
+
image_name = File.basename(image_path)
|
240
|
+
zipfile.add(image_name, image_path)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
upload_url = @profiler.measure('create_batch_upload_url') do
|
246
|
+
response = @network.post(path: '/v1/snapshots/run/batch-image', body: { run_id: run_id })
|
247
|
+
JSON.parse(response.read).fetch('zip_url')
|
248
|
+
end
|
249
|
+
|
250
|
+
Logger.info 'Uploading images...'
|
251
|
+
Logger.debug "Uploading batch zip file to #{upload_url}"
|
252
|
+
@profiler.measure('upload_batch_zip') do
|
253
|
+
@network.put(
|
254
|
+
path: upload_url,
|
255
|
+
headers: { 'Content-Type' => 'application/zip' },
|
256
|
+
body: File.read(zip_file.path)
|
257
|
+
)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
ensure
|
261
|
+
metadata_barrier&.stop
|
262
|
+
end
|
263
|
+
|
264
|
+
def individual_upload_images(run_id, concurrency, image_files, client)
|
181
265
|
post_image_barrier = Async::Barrier.new
|
182
266
|
post_image_semaphore = Async::Semaphore.new(concurrency, parent: post_image_barrier)
|
183
267
|
|
data/lib/emerge_cli.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
1
|
require_relative 'version'
|
2
2
|
|
3
3
|
require_relative 'commands/global_options'
|
4
|
-
require_relative 'commands/
|
5
|
-
require_relative 'commands/
|
6
|
-
require_relative 'commands/upload/snapshots/client_libraries/paparazzi'
|
7
|
-
require_relative 'commands/upload/snapshots/client_libraries/roborazzi'
|
8
|
-
require_relative 'commands/upload/snapshots/client_libraries/default'
|
9
|
-
require_relative 'commands/integrate/fastlane'
|
4
|
+
require_relative 'commands/build/distribution/validate'
|
5
|
+
require_relative 'commands/build/distribution/install'
|
10
6
|
require_relative 'commands/config/snapshots/snapshots_ios'
|
11
7
|
require_relative 'commands/config/orderfiles/orderfiles_ios'
|
12
|
-
require_relative 'commands/
|
13
|
-
require_relative 'commands/
|
8
|
+
require_relative 'commands/integrate/fastlane'
|
9
|
+
require_relative 'commands/fix/minify_strings'
|
10
|
+
require_relative 'commands/fix/strip_binary_symbols'
|
11
|
+
require_relative 'commands/fix/exported_symbols'
|
14
12
|
require_relative 'commands/order_files/download_order_files'
|
15
13
|
require_relative 'commands/order_files/validate_linkmaps'
|
16
14
|
require_relative 'commands/order_files/validate_xcode_project'
|
15
|
+
require_relative 'commands/reaper/reaper'
|
16
|
+
require_relative 'commands/snapshots/validate_app'
|
17
17
|
require_relative 'commands/upload/build'
|
18
|
-
require_relative 'commands/
|
19
|
-
require_relative 'commands/
|
20
|
-
require_relative 'commands/
|
21
|
-
require_relative 'commands/
|
22
|
-
require_relative 'commands/
|
18
|
+
require_relative 'commands/upload/snapshots/snapshots'
|
19
|
+
require_relative 'commands/upload/snapshots/client_libraries/swift_snapshot_testing'
|
20
|
+
require_relative 'commands/upload/snapshots/client_libraries/paparazzi'
|
21
|
+
require_relative 'commands/upload/snapshots/client_libraries/roborazzi'
|
22
|
+
require_relative 'commands/upload/snapshots/client_libraries/default'
|
23
23
|
|
24
24
|
require_relative 'reaper/ast_parser'
|
25
25
|
require_relative 'reaper/code_deleter'
|
@@ -44,41 +44,42 @@ require 'dry/cli'
|
|
44
44
|
module EmergeCLI
|
45
45
|
extend Dry::CLI::Registry
|
46
46
|
|
47
|
-
register 'upload', aliases: ['u'] do |prefix|
|
48
|
-
prefix.register 'build', Commands::Upload::Build
|
49
|
-
prefix.register 'snapshots', Commands::Upload::Snapshots
|
50
|
-
end
|
51
|
-
|
52
|
-
register 'integrate' do |prefix|
|
53
|
-
prefix.register 'fastlane-ios', Commands::Integrate::Fastlane, aliases: ['i']
|
54
|
-
end
|
55
|
-
|
56
47
|
register 'configure' do |prefix|
|
57
48
|
prefix.register 'snapshots-ios', Commands::Config::SnapshotsIOS
|
58
49
|
prefix.register 'order-files-ios', Commands::Config::OrderFilesIOS
|
59
50
|
end
|
60
51
|
|
61
|
-
register '
|
52
|
+
register 'download' do |prefix|
|
53
|
+
prefix.register 'order-files', Commands::OrderFiles::Download
|
54
|
+
end
|
62
55
|
|
63
|
-
register '
|
64
|
-
prefix.register '
|
56
|
+
register 'fix' do |prefix|
|
57
|
+
prefix.register 'minify-strings', Commands::Fix::MinifyStrings
|
58
|
+
prefix.register 'strip-binary-symbols', Commands::Fix::StripBinarySymbols
|
59
|
+
prefix.register 'exported-symbols', Commands::Fix::ExportedSymbols
|
65
60
|
end
|
66
61
|
|
67
|
-
register '
|
68
|
-
prefix.register '
|
69
|
-
prefix.register 'validate-linkmaps', Commands::ValidateLinkmaps
|
70
|
-
prefix.register 'validate-xcode-project', Commands::ValidateXcodeProject
|
62
|
+
register 'integrate' do |prefix|
|
63
|
+
prefix.register 'fastlane-ios', Commands::Integrate::Fastlane, aliases: ['i']
|
71
64
|
end
|
72
65
|
|
73
|
-
register '
|
74
|
-
prefix.register '
|
75
|
-
|
66
|
+
register 'install' do |prefix|
|
67
|
+
prefix.register 'build', Commands::Build::Distribution::Install
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO: make this command action oriented
|
71
|
+
register 'reaper', Commands::Reaper
|
72
|
+
|
73
|
+
register 'upload', aliases: ['u'] do |prefix|
|
74
|
+
prefix.register 'build', Commands::Upload::Build
|
75
|
+
prefix.register 'snapshots', Commands::Upload::Snapshots
|
76
76
|
end
|
77
77
|
|
78
|
-
register '
|
79
|
-
prefix.register '
|
80
|
-
prefix.register '
|
81
|
-
prefix.register '
|
78
|
+
register 'validate' do |prefix|
|
79
|
+
prefix.register 'build-distribution', Commands::Build::Distribution::ValidateApp
|
80
|
+
prefix.register 'order-files-linkmaps', Commands::OrderFiles::ValidateLinkmaps
|
81
|
+
prefix.register 'order-files-xcode-project', Commands::OrderFiles::ValidateXcodeProject
|
82
|
+
prefix.register 'snapshots-app-ios', Commands::Snapshots::ValidateApp
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
data/lib/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: emerge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emerge Tools
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async-http
|
@@ -209,13 +209,13 @@ files:
|
|
209
209
|
- CHANGELOG.md
|
210
210
|
- README.md
|
211
211
|
- exe/emerge
|
212
|
-
- lib/commands/
|
213
|
-
- lib/commands/
|
214
|
-
- lib/commands/autofixes/strip_binary_symbols.rb
|
215
|
-
- lib/commands/build_distribution/download_and_install.rb
|
216
|
-
- lib/commands/build_distribution/validate_app.rb
|
212
|
+
- lib/commands/build/distribution/install.rb
|
213
|
+
- lib/commands/build/distribution/validate.rb
|
217
214
|
- lib/commands/config/orderfiles/orderfiles_ios.rb
|
218
215
|
- lib/commands/config/snapshots/snapshots_ios.rb
|
216
|
+
- lib/commands/fix/exported_symbols.rb
|
217
|
+
- lib/commands/fix/minify_strings.rb
|
218
|
+
- lib/commands/fix/strip_binary_symbols.rb
|
219
219
|
- lib/commands/global_options.rb
|
220
220
|
- lib/commands/integrate/fastlane.rb
|
221
221
|
- lib/commands/order_files/download_order_files.rb
|
@@ -1,139 +0,0 @@
|
|
1
|
-
require 'dry/cli'
|
2
|
-
require 'cfpropertylist'
|
3
|
-
require 'zip'
|
4
|
-
require 'rbconfig'
|
5
|
-
require 'tmpdir'
|
6
|
-
|
7
|
-
module EmergeCLI
|
8
|
-
module Commands
|
9
|
-
module BuildDistribution
|
10
|
-
class DownloadAndInstall < EmergeCLI::Commands::GlobalOptions
|
11
|
-
desc 'Download build from Build Distribution'
|
12
|
-
|
13
|
-
option :api_token, type: :string, required: false,
|
14
|
-
desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
|
15
|
-
option :build_id, type: :string, required: true, desc: 'Build ID to download'
|
16
|
-
option :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
|
17
|
-
option :device_id, type: :string, desc: 'Specific device ID to target'
|
18
|
-
option :device_type, type: :string, enum: %w[virtual physical any], default: 'any',
|
19
|
-
desc: 'Type of device to target (virtual/physical/any)'
|
20
|
-
option :output, type: :string, required: false, desc: 'Output path for the downloaded build'
|
21
|
-
|
22
|
-
def initialize(network: nil)
|
23
|
-
@network = network
|
24
|
-
end
|
25
|
-
|
26
|
-
def call(**options)
|
27
|
-
@options = options
|
28
|
-
before(options)
|
29
|
-
|
30
|
-
Sync do
|
31
|
-
api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
|
32
|
-
raise 'API token is required' unless api_token
|
33
|
-
|
34
|
-
raise 'Build ID is required' unless @options[:build_id]
|
35
|
-
|
36
|
-
output_name = nil
|
37
|
-
app_id = nil
|
38
|
-
|
39
|
-
begin
|
40
|
-
@network ||= EmergeCLI::Network.new(api_token:)
|
41
|
-
|
42
|
-
Logger.info 'Getting build URL...'
|
43
|
-
request = get_build_url(@options[:build_id])
|
44
|
-
response = parse_response(request)
|
45
|
-
|
46
|
-
platform = response['platform']
|
47
|
-
download_url = response['downloadUrl']
|
48
|
-
app_id = response['appId']
|
49
|
-
|
50
|
-
extension = platform == 'ios' ? 'ipa' : 'apk'
|
51
|
-
Logger.info 'Downloading build...'
|
52
|
-
output_name = @options[:output] || "#{@options[:build_id]}.#{extension}"
|
53
|
-
`curl --progress-bar -L '#{download_url}' -o #{output_name} `
|
54
|
-
Logger.info "✅ Build downloaded to #{output_name}"
|
55
|
-
rescue StandardError => e
|
56
|
-
Logger.error "❌ Failed to download build: #{e.message}"
|
57
|
-
raise e
|
58
|
-
ensure
|
59
|
-
@network&.close
|
60
|
-
end
|
61
|
-
|
62
|
-
begin
|
63
|
-
if @options[:install] && !output_name.nil?
|
64
|
-
if platform == 'ios'
|
65
|
-
install_ios_build(output_name, app_id)
|
66
|
-
elsif platform == 'android'
|
67
|
-
install_android_build(output_name)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
rescue StandardError => e
|
71
|
-
Logger.error "❌ Failed to install build: #{e.message}"
|
72
|
-
raise e
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
|
79
|
-
def get_build_url(build_id)
|
80
|
-
@network.get(
|
81
|
-
path: '/distribution/downloadUrl',
|
82
|
-
max_retries: 3,
|
83
|
-
query: {
|
84
|
-
buildId: build_id
|
85
|
-
}
|
86
|
-
)
|
87
|
-
end
|
88
|
-
|
89
|
-
def parse_response(response)
|
90
|
-
case response.status
|
91
|
-
when 200
|
92
|
-
JSON.parse(response.read)
|
93
|
-
when 400
|
94
|
-
error_message = JSON.parse(response.read)['errorMessage']
|
95
|
-
raise "Invalid parameters: #{error_message}"
|
96
|
-
when 401, 403
|
97
|
-
raise 'Invalid API token'
|
98
|
-
else
|
99
|
-
raise "Getting build failed with status #{response.status}"
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def install_ios_build(build_path, app_id)
|
104
|
-
device_type = case @options[:device_type]
|
105
|
-
when 'simulator'
|
106
|
-
XcodeDeviceManager::DeviceType::VIRTUAL
|
107
|
-
when 'physical'
|
108
|
-
XcodeDeviceManager::DeviceType::PHYSICAL
|
109
|
-
else
|
110
|
-
XcodeDeviceManager::DeviceType::ANY
|
111
|
-
end
|
112
|
-
|
113
|
-
device_manager = XcodeDeviceManager.new
|
114
|
-
device = if @options[:device_id]
|
115
|
-
device_manager.find_device_by_id(@options[:device_id])
|
116
|
-
else
|
117
|
-
device_manager.find_device_by_type(device_type, build_path)
|
118
|
-
end
|
119
|
-
|
120
|
-
Logger.info "Installing build on #{device.device_id}"
|
121
|
-
device.install_app(build_path)
|
122
|
-
Logger.info '✅ Build installed'
|
123
|
-
|
124
|
-
Logger.info "Launching app #{app_id}..."
|
125
|
-
device.launch_app(app_id)
|
126
|
-
Logger.info '✅ Build launched'
|
127
|
-
end
|
128
|
-
|
129
|
-
def install_android_build(build_path)
|
130
|
-
command = "adb -s #{@options[:device_id]} install #{build_path}"
|
131
|
-
Logger.debug "Running command: #{command}"
|
132
|
-
`#{command}`
|
133
|
-
|
134
|
-
Logger.info '✅ Build installed'
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
@@ -1,164 +0,0 @@
|
|
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
|