emerge 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bc78d26069b4e3ef6c4154958eab2ba668ef0e2074d10bfca920dea1767d742
4
- data.tar.gz: 6820120287c28ff285e819a5416c7db6568331664e138402d5b03660e1913dcd
3
+ metadata.gz: f526af7bee28fe19586eb432339f8b6d771e98c9127a5126a4be9f47a32d6ab7
4
+ data.tar.gz: eb356b6d63d1e66595550516aa78c36d7926b867ac5caf9a35000f0c8b3feeb5
5
5
  SHA512:
6
- metadata.gz: 699e8dfac1ad70f312261ccd34790185e1d0f72ca84b25574470c1b312870da04add5f3c6ce3f51a89840ce21d2713ba27641552345960608dc991c61bf005a1
7
- data.tar.gz: 9094c02b3add07de9fe9a0309a55cf133ea9f4f77d0ef1064ce15703e366a940987ddf39d21f8957d2e41dfacf2021d98dd9c89210b8fe49f8c88c2f8d3f8d13
6
+ metadata.gz: 5e3ba42f1367adf1b50e0bb236b79dc58225df5bd29e7e3af419775cac52e2d04415d5c949b0a62f2eb17f0ae5d25bd82cc086f194d34da0f912dd35af0e433a
7
+ data.tar.gz: adf35a55cf1e66d9b0de879e8bab9843c2da10d3a00784dc2eebab7f865ac6ca501be7f81a28c660c6de60ff6ffb4f8e0670fbaeaf4057a67c943424759c8855
@@ -0,0 +1,141 @@
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 Build
10
+ module Distribution
11
+ class Install < EmergeCLI::Commands::GlobalOptions
12
+ desc 'Download and install a build from Build Distribution'
13
+
14
+ option :api_token, type: :string, required: false,
15
+ desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
16
+ option :build_id, type: :string, required: true, desc: 'Build ID to download'
17
+ option :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
18
+ option :device_id, type: :string, desc: 'Specific device ID to target'
19
+ option :device_type, type: :string, enum: %w[virtual physical any], default: 'any',
20
+ desc: 'Type of device to target (virtual/physical/any)'
21
+ option :output, type: :string, required: false, desc: 'Output path for the downloaded build'
22
+
23
+ def initialize(network: nil)
24
+ @network = network
25
+ end
26
+
27
+ def call(**options)
28
+ @options = options
29
+ before(options)
30
+
31
+ Sync do
32
+ api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
33
+ raise 'API token is required' unless api_token
34
+
35
+ raise 'Build ID is required' unless @options[:build_id]
36
+
37
+ output_name = nil
38
+ app_id = nil
39
+
40
+ begin
41
+ @network ||= EmergeCLI::Network.new(api_token:)
42
+
43
+ Logger.info 'Getting build URL...'
44
+ request = get_build_url(@options[:build_id])
45
+ response = parse_response(request)
46
+
47
+ platform = response['platform']
48
+ download_url = response['downloadUrl']
49
+ app_id = response['appId']
50
+
51
+ extension = platform == 'ios' ? 'ipa' : 'apk'
52
+ Logger.info 'Downloading build...'
53
+ output_name = @options[:output] || "#{@options[:build_id]}.#{extension}"
54
+ `curl --progress-bar -L '#{download_url}' -o #{output_name} `
55
+ Logger.info "✅ Build downloaded to #{output_name}"
56
+ rescue StandardError => e
57
+ Logger.error "❌ Failed to download build: #{e.message}"
58
+ raise e
59
+ ensure
60
+ @network&.close
61
+ end
62
+
63
+ begin
64
+ if @options[:install] && !output_name.nil?
65
+ if platform == 'ios'
66
+ install_ios_build(output_name, app_id)
67
+ elsif platform == 'android'
68
+ install_android_build(output_name)
69
+ end
70
+ end
71
+ rescue StandardError => e
72
+ Logger.error "❌ Failed to install build: #{e.message}"
73
+ raise e
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def get_build_url(build_id)
81
+ @network.get(
82
+ path: '/distribution/downloadUrl',
83
+ max_retries: 3,
84
+ query: {
85
+ buildId: build_id
86
+ }
87
+ )
88
+ end
89
+
90
+ def parse_response(response)
91
+ case response.status
92
+ when 200
93
+ JSON.parse(response.read)
94
+ when 400
95
+ error_message = JSON.parse(response.read)['errorMessage']
96
+ raise "Invalid parameters: #{error_message}"
97
+ when 401, 403
98
+ raise 'Invalid API token'
99
+ else
100
+ raise "Getting build failed with status #{response.status}"
101
+ end
102
+ end
103
+
104
+ def install_ios_build(build_path, app_id)
105
+ device_type = case @options[:device_type]
106
+ when 'simulator'
107
+ XcodeDeviceManager::DeviceType::VIRTUAL
108
+ when 'physical'
109
+ XcodeDeviceManager::DeviceType::PHYSICAL
110
+ else
111
+ XcodeDeviceManager::DeviceType::ANY
112
+ end
113
+
114
+ device_manager = XcodeDeviceManager.new
115
+ device = if @options[:device_id]
116
+ device_manager.find_device_by_id(@options[:device_id])
117
+ else
118
+ device_manager.find_device_by_type(device_type, build_path)
119
+ end
120
+
121
+ Logger.info "Installing build on #{device.device_id}"
122
+ device.install_app(build_path)
123
+ Logger.info '✅ Build installed'
124
+
125
+ Logger.info "Launching app #{app_id}..."
126
+ device.launch_app(app_id)
127
+ Logger.info '✅ Build launched'
128
+ end
129
+
130
+ def install_android_build(build_path)
131
+ command = "adb -s #{@options[:device_id]} install #{build_path}"
132
+ Logger.debug "Running command: #{command}"
133
+ `#{command}`
134
+
135
+ Logger.info '✅ Build installed'
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ 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
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
 
@@ -29,6 +29,29 @@ module EmergeCLI
29
29
 
30
30
  attr_reader :parser, :language
31
31
 
32
+ @parser_paths_cache = {}
33
+
34
+ class << self
35
+ def find_parser_path(language, platform, arch)
36
+ cache_key = "#{language}-#{platform}-#{arch}"
37
+ return @parser_paths_cache[cache_key] if @parser_paths_cache.key?(cache_key)
38
+
39
+ extension = platform == 'darwin' ? 'dylib' : 'so'
40
+ parser_file = "libtree-sitter-#{language}-#{platform}-#{arch}.#{extension}"
41
+
42
+ parser_paths = [
43
+ File.join(File.dirname(__FILE__), '..', '..', 'parsers', parser_file), # Relative to this file
44
+ File.join(Gem::Specification.find_by_name('emerge').gem_dir, 'parsers', parser_file) # Installed gem path
45
+ ]
46
+
47
+ parser_path = parser_paths.find { |path| File.exist?(path) }
48
+ raise "No language grammar found for #{language}. Searched in: #{parser_paths.join(', ')}" unless parser_path
49
+
50
+ @parser_paths_cache[cache_key] = parser_path
51
+ parser_path
52
+ end
53
+ end
54
+
32
55
  def initialize(language)
33
56
  @parser = TreeSitter::Parser.new
34
57
  @language = language
@@ -52,11 +75,7 @@ module EmergeCLI
52
75
  raise "Unsupported architecture: #{RUBY_PLATFORM}"
53
76
  end
54
77
 
55
- extension = platform == 'darwin' ? 'dylib' : 'so'
56
- parser_file = "libtree-sitter-#{language}-#{platform}-#{arch}.#{extension}"
57
- parser_path = File.join('parsers', parser_file)
58
- raise "No language grammar found for #{language}" unless File.exist?(parser_path)
59
-
78
+ parser_path = self.class.find_parser_path(language, platform, arch)
60
79
  @parser.language = TreeSitter::Language.load(language, parser_path)
61
80
  end
62
81
 
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module EmergeCLI
2
- VERSION = '0.6.1'.freeze
2
+ VERSION = '0.7.0'.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.1
4
+ version: 0.7.0
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-23 00:00:00.000000000 Z
11
+ date: 2025-01-31 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