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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 615b959713db3bc2be8d96888f8d6de83aa903ef7d20ac8d4516962f627a4b93
4
- data.tar.gz: ef95d430576322ebf5810296438aa5f67ff754d99ac2be9520a4978b94caad6e
3
+ metadata.gz: 409fec8581e499747b3c9ca5cd80e0f9c9e491ca0d94688bc1e0d299a974a95b
4
+ data.tar.gz: 64863c523c84b25f5ff21a9bbda0863f13ef7a5165d5437aeb66d60211e7fe84
5
5
  SHA512:
6
- metadata.gz: cb5d892cf29c7994dcc9139ed8dbda403c1cf2b376e1370a5be993aef9f0d030b9335f34544a1a41a4dc6d8fc05d9a13740e6f530412ab502d4e68117e3e197d
7
- data.tar.gz: afd1dba61977e93757b1d19086f6ae43c3436dcf3efe193b9d0c7eab671e00dffbb41c42611516283e280607ac5a2b869d9cbc51a8a11d76b53f6f4fb526f265
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
@@ -3,7 +3,7 @@ require 'xcodeproj'
3
3
 
4
4
  module EmergeCLI
5
5
  module Commands
6
- module Autofixes
6
+ module Fix
7
7
  class ExportedSymbols < EmergeCLI::Commands::GlobalOptions
8
8
  desc 'Remove exported symbols from built binaries'
9
9
 
@@ -3,7 +3,7 @@ require 'xcodeproj'
3
3
 
4
4
  module EmergeCLI
5
5
  module Commands
6
- module Autofixes
6
+ module Fix
7
7
  class MinifyStrings < EmergeCLI::Commands::GlobalOptions
8
8
  desc 'Minify strings in the app'
9
9
 
@@ -3,7 +3,7 @@ require 'xcodeproj'
3
3
 
4
4
  module EmergeCLI
5
5
  module Commands
6
- module Autofixes
6
+ module Fix
7
7
  class StripBinarySymbols < EmergeCLI::Commands::GlobalOptions
8
8
  desc 'Strip binary symbols from the app'
9
9
 
@@ -2,75 +2,77 @@ require 'dry/cli'
2
2
 
3
3
  module EmergeCLI
4
4
  module Commands
5
- class DownloadOrderFiles < EmergeCLI::Commands::GlobalOptions
6
- desc 'Download order files from Emerge'
5
+ module OrderFiles
6
+ class Download < EmergeCLI::Commands::GlobalOptions
7
+ desc 'Download order files from Emerge'
7
8
 
8
- option :bundle_id, type: :string, required: true, desc: 'Bundle identifier to download order files for'
9
+ option :bundle_id, type: :string, required: true, desc: 'Bundle identifier to download order files for'
9
10
 
10
- option :api_token, type: :string, required: false,
11
- desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
11
+ option :api_token, type: :string, required: false,
12
+ desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
12
13
 
13
- option :app_version, type: :string, required: true,
14
- desc: 'App version to download order files for'
14
+ option :app_version, type: :string, required: true,
15
+ desc: 'App version to download order files for'
15
16
 
16
- option :unzip, type: :boolean, required: false,
17
- desc: 'Unzip the order file after downloading'
17
+ option :unzip, type: :boolean, required: false,
18
+ desc: 'Unzip the order file after downloading'
18
19
 
19
- option :output, type: :string, required: false,
20
- desc: 'Output name for the order file, defaults to bundle_id-app_version.gz'
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
- EMERGE_ORDER_FILE_URL = 'order-files-prod.emergetools.com'.freeze
23
+ EMERGE_ORDER_FILE_URL = 'order-files-prod.emergetools.com'.freeze
23
24
 
24
- def initialize(network: nil)
25
- @network = network
26
- end
25
+ def initialize(network: nil)
26
+ @network = network
27
+ end
27
28
 
28
- def call(**options)
29
- @options = options
30
- before(options)
29
+ def call(**options)
30
+ @options = options
31
+ before(options)
31
32
 
32
- begin
33
- api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
34
- raise 'API token is required' unless api_token
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
- raise 'Bundle ID is required' unless @options[:bundle_id]
37
- raise 'App version is required' unless @options[:app_version]
37
+ raise 'Bundle ID is required' unless @options[:bundle_id]
38
+ raise 'App version is required' unless @options[:app_version]
38
39
 
39
- @network ||= EmergeCLI::Network.new(api_token:, base_url: EMERGE_ORDER_FILE_URL)
40
- output_name = @options[:output] || "#{@options[:bundle_id]}-#{@options[:app_version]}.gz"
41
- output_name = "#{output_name}.gz" unless output_name.end_with?('.gz')
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
- Sync do
44
- request = get_order_file(options[:bundle_id], options[:app_version])
45
- response = request.read
44
+ Sync do
45
+ request = get_order_file(options[:bundle_id], options[:app_version])
46
+ response = request.read
46
47
 
47
- File.write(output_name, response)
48
+ File.write(output_name, response)
48
49
 
49
- if @options[:unzip]
50
- Logger.info 'Unzipping order file...'
51
- Zlib::GzipReader.open(output_name) do |gz|
52
- File.write(output_name.gsub('.gz', ''), gz.read)
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
- Logger.info 'Order file downloaded successfully'
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
- private
68
+ private
68
69
 
69
- def get_order_file(bundle_id, app_version)
70
- @network.get(
71
- path: "/#{bundle_id}/#{app_version}",
72
- max_retries: 0
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
- class ValidateLinkmaps < EmergeCLI::Commands::GlobalOptions
7
- desc 'Validate linkmaps in xcarchive'
6
+ module OrderFiles
7
+ class ValidateLinkmaps < EmergeCLI::Commands::GlobalOptions
8
+ desc 'Validate linkmaps in xcarchive'
8
9
 
9
- option :path, type: :string, required: true, desc: 'Path to the xcarchive to validate'
10
+ option :path, type: :string, required: true, desc: 'Path to the xcarchive to validate'
10
11
 
11
- def initialize(network: nil)
12
- @network = network
13
- end
12
+ def initialize(network: nil)
13
+ @network = network
14
+ end
14
15
 
15
- def call(**options)
16
- @options = options
17
- before(options)
16
+ def call(**options)
17
+ @options = options
18
+ before(options)
18
19
 
19
- Sync do
20
- executable_name = get_executable_name
21
- raise 'Executable not found' if executable_name.nil?
20
+ Sync do
21
+ executable_name = get_executable_name
22
+ raise 'Executable not found' if executable_name.nil?
22
23
 
23
- Logger.info "Using executable: #{executable_name}"
24
+ Logger.info "Using executable: #{executable_name}"
24
25
 
25
- linkmaps_path = File.join(@options[:path], 'Linkmaps')
26
- raise 'Linkmaps folder not found' unless File.directory?(linkmaps_path)
26
+ linkmaps_path = File.join(@options[:path], 'Linkmaps')
27
+ raise 'Linkmaps folder not found' unless File.directory?(linkmaps_path)
27
28
 
28
- linkmaps = Dir.glob("#{linkmaps_path}/*.txt")
29
- raise 'No linkmaps found' if linkmaps.empty?
29
+ linkmaps = Dir.glob("#{linkmaps_path}/*.txt")
30
+ raise 'No linkmaps found' if linkmaps.empty?
30
31
 
31
- executable_linkmaps = linkmaps.select do |linkmap|
32
- File.basename(linkmap).start_with?(executable_name)
33
- end
34
- raise 'No linkmaps found for executable' if executable_linkmaps.empty?
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
- Logger.info "✅ Found linkmaps for #{executable_name}"
37
+ Logger.info "✅ Found linkmaps for #{executable_name}"
38
+ end
37
39
  end
38
- end
39
40
 
40
- private
41
+ private
41
42
 
42
- def get_executable_name
43
- raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
43
+ def get_executable_name
44
+ raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
44
45
 
45
- app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
46
- info_path = File.join(app_path, 'Info.plist')
47
- plist_data = File.read(info_path)
48
- plist = CFPropertyList::List.new(data: plist_data)
49
- parsed_data = CFPropertyList.native_types(plist.value)
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
- parsed_data['CFBundleExecutable']
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
- class ValidateXcodeProject < EmergeCLI::Commands::GlobalOptions
7
- desc 'Validate xcodeproject for order files'
6
+ module OrderFiles
7
+ class ValidateXcodeProject < EmergeCLI::Commands::GlobalOptions
8
+ desc 'Validate xcodeproject for order files'
8
9
 
9
- option :path, type: :string, required: true, desc: 'Path to the xcodeproject to validate'
10
- option :target, type: :string, required: false, desc: 'Target to validate'
11
- option :build_configuration, type: :string, required: false,
12
- desc: 'Build configuration to validate (Release by default)'
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
- # Constants
15
- LINK_MAPS_CONFIG = 'LD_GENERATE_MAP_FILE'.freeze
16
- LINK_MAPS_PATH = 'LD_MAP_FILE_PATH'.freeze
17
- PATH_TO_LINKMAP = '$(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt'.freeze
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
- def call(**options)
20
- @options = options
21
- before(options)
20
+ def call(**options)
21
+ @options = options
22
+ before(options)
22
23
 
23
- raise 'Path must be an xcodeproject' unless @options[:path].end_with?('.xcodeproj')
24
- raise 'Path does not exist' unless File.exist?(@options[:path])
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
- @options[:build_configuration] ||= 'Release'
27
+ @options[:build_configuration] ||= 'Release'
27
28
 
28
- Sync do
29
- project = Xcodeproj::Project.open(@options[:path])
29
+ Sync do
30
+ project = Xcodeproj::Project.open(@options[:path])
30
31
 
31
- validate_xcproj(project)
32
+ validate_xcproj(project)
33
+ end
32
34
  end
33
- end
34
35
 
35
- private
36
+ private
36
37
 
37
- def validate_xcproj(project)
38
- project.targets.each do |target|
39
- next if @options[:target] && target.name != @options[:target]
40
- next unless target.product_type == 'com.apple.product-type.application'
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
- target.build_configurations.each do |config|
43
- next if config.name != @options[:build_configuration]
44
- validate_target_config(target, config)
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
- def validate_target_config(target, config)
50
- has_error = false
51
- if config.build_settings[LINK_MAPS_CONFIG] != 'YES'
52
- has_error = true
53
- Logger.error "❌ Write Link Map File (#{LINK_MAPS_CONFIG}) is not set to YES"
54
- end
55
- if config.build_settings[LINK_MAPS_PATH] != ''
56
- has_error = true
57
- Logger.error "❌ Path to Link Map File (#{LINK_MAPS_PATH}) is not set, we recommend \
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
- end
60
+ end
60
61
 
61
- if has_error
62
- Logger.error "❌ Target '#{target.name}' has errors, this means \
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
- Logger.error "Use `emerge configure order-files-ios --project-path '#{@options[:path]}'` to fix this"
65
- else
66
- Logger.info "✅ Target '#{target.name}' is valid"
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 :upload_id, type: :string, required: true, desc: 'Upload ID to analyze'
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[:upload_id]) }
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/upload/snapshots/snapshots'
5
- require_relative 'commands/upload/snapshots/client_libraries/swift_snapshot_testing'
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/reaper/reaper'
13
- require_relative 'commands/snapshots/validate_app'
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/build_distribution/validate_app'
19
- require_relative 'commands/build_distribution/download_and_install'
20
- require_relative 'commands/autofixes/minify_strings'
21
- require_relative 'commands/autofixes/strip_binary_symbols'
22
- require_relative 'commands/autofixes/exported_symbols'
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 'reaper', Commands::Reaper
52
+ register 'download' do |prefix|
53
+ prefix.register 'order-files', Commands::OrderFiles::Download
54
+ end
62
55
 
63
- register 'snapshots' do |prefix|
64
- prefix.register 'validate-app-ios', Commands::Snapshots::ValidateApp
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 'order-files' do |prefix|
68
- prefix.register 'download', Commands::DownloadOrderFiles
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 'build-distribution' do |prefix|
74
- prefix.register 'validate-app', Commands::BuildDistribution::ValidateApp
75
- prefix.register 'install', Commands::BuildDistribution::DownloadAndInstall
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 'autofix' do |prefix|
79
- prefix.register 'minify-strings', Commands::Autofixes::MinifyStrings
80
- prefix.register 'strip-binary-symbols', Commands::Autofixes::StripBinarySymbols
81
- prefix.register 'exported-symbols', Commands::Autofixes::ExportedSymbols
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
@@ -1,3 +1,3 @@
1
1
  module EmergeCLI
2
- VERSION = '0.6.2'.freeze
2
+ VERSION = '0.7.1'.freeze
3
3
  end
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.6.2
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-01-27 00:00:00.000000000 Z
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/autofixes/exported_symbols.rb
213
- - lib/commands/autofixes/minify_strings.rb
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