emerge 0.3.0 → 0.5.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: f78e2be6724a6620a5135d20910b99fe76c8a959b00f6703bdad0c0d2c043742
4
- data.tar.gz: 8366d036be25ec0b3da721346b1c71dcfcdccd2bf7b7f2bea9f3e6838cf62a8b
3
+ metadata.gz: 1ebd308782a251e11a4d515c0728b572456ed730ef2376f634939fec6ace820e
4
+ data.tar.gz: d476865caa4a62c32d9b258ec6fbfb474cb67c7bdf05c3ef1548ff860a1a9697
5
5
  SHA512:
6
- metadata.gz: 4359ea66070503dc5dc98d1040a39598df3d760bad78393724baa0f689caf18c5e26d2875375ba3751f2fb95d4641e078ff4111084c7acf51b63118955e6a3cc
7
- data.tar.gz: 13d4cc4f14a5f6e794592ce12eb5f6acd785fa5d51fbb7d9241e69c78ab7317a5f7b374dbcf06b2bf641cff890b1a7baa941ad3c76f306e207bbc069492875f0
6
+ metadata.gz: 149980818e3fcb6741c029a601cb903dcd05051a4564889d41b8e49c4380a7f08662b26cbfe5eca0e3ced6b114a12075118fd749e88ccb27ec694a4352799f8d
7
+ data.tar.gz: d1ed903d5c8560a649fd4566a45067403fe506112d79ff6e686763bfee02d8c7c9a2823fc7a23fc5af4cf93f5c6a20fb82d59644fd3347b837256df43b09abae
@@ -0,0 +1,62 @@
1
+ require 'dry/cli'
2
+ require 'xcodeproj'
3
+
4
+ module EmergeCLI
5
+ module Commands
6
+ module Autofixes
7
+ class ExportedSymbols < EmergeCLI::Commands::GlobalOptions
8
+ desc 'Remove exported symbols from built binaries'
9
+
10
+ option :path, type: :string, required: true, desc: 'Path to the xcarchive'
11
+
12
+ # Constants
13
+ DEFAULT_EXPORTED_SYMBOLS = %(_main
14
+ __mh_execute_header).freeze
15
+ EXPORTED_SYMBOLS_FILE = 'EXPORTED_SYMBOLS_FILE'.freeze
16
+ EXPORTED_SYMBOLS_PATH = '$(SRCROOT)/EmergeToolsHelperFiles/ExportedSymbols'.freeze
17
+ EXPORTED_SYMBOLS_FILE_NAME = 'ExportedSymbols'.freeze
18
+ EMERGE_TOOLS_GROUP = 'EmergeToolsHelperFiles'.freeze
19
+
20
+ def call(**options)
21
+ @options = options
22
+ before(options)
23
+
24
+ raise 'Path must be an xcodeproj' unless @options[:path].end_with?('.xcodeproj')
25
+ raise 'Path does not exist' unless File.exist?(@options[:path])
26
+
27
+ Sync do
28
+ project = Xcodeproj::Project.open(@options[:path])
29
+
30
+ # Add the exported symbols file to the project
31
+ group = project.main_group
32
+ emergetools_group = group.find_subpath(EMERGE_TOOLS_GROUP, true)
33
+ emergetools_group.set_path(EMERGE_TOOLS_GROUP)
34
+
35
+ unless emergetools_group.find_file_by_path(EXPORTED_SYMBOLS_FILE_NAME)
36
+ emergetools_group.new_file(EXPORTED_SYMBOLS_FILE_NAME)
37
+ end
38
+
39
+ # Create Folder if it doesn't exist
40
+
41
+ FileUtils.mkdir_p(File.join(File.dirname(@options[:path]), EMERGE_TOOLS_GROUP))
42
+
43
+ # Create the exported symbols file
44
+ path = File.join(File.dirname(@options[:path]), EMERGE_TOOLS_GROUP, EXPORTED_SYMBOLS_FILE_NAME)
45
+ File.write(path, DEFAULT_EXPORTED_SYMBOLS)
46
+
47
+ project.targets.each do |target|
48
+ # Only do it for app targets
49
+ next unless target.product_type == 'com.apple.product-type.application'
50
+
51
+ target.build_configurations.each do |config|
52
+ config.build_settings[EXPORTED_SYMBOLS_FILE] = EXPORTED_SYMBOLS_PATH
53
+ end
54
+ end
55
+
56
+ project.save
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,99 @@
1
+ require 'dry/cli'
2
+ require 'xcodeproj'
3
+
4
+ module EmergeCLI
5
+ module Commands
6
+ module Autofixes
7
+ class MinifyStrings < EmergeCLI::Commands::GlobalOptions
8
+ desc 'Minify strings in the app'
9
+
10
+ option :path, type: :string, required: true, desc: 'Path to the xcarchive'
11
+
12
+ # Constants
13
+ SCRIPT_NAME = 'EmergeTools Minify Strings'.freeze
14
+ ENABLE_USER_SCRIPT_SANDBOXING = 'ENABLE_USER_SCRIPT_SANDBOXING'.freeze
15
+ STRINGS_FILE_OUTPUT_ENCODING = 'STRINGS_FILE_OUTPUT_ENCODING'.freeze
16
+ STRINGS_FILE_OUTPUT_ENCODING_VALUE = 'UTF-8'.freeze
17
+ SCRIPT_CONTENT = %{import os
18
+ import json
19
+ from multiprocessing.pool import ThreadPool
20
+
21
+ def minify(file_path):
22
+ os.system(f"plutil -convert json '{file_path}'")
23
+ new_content = ''
24
+ try:
25
+ with open(file_path, 'r') as input_file:
26
+ data = json.load(input_file)
27
+
28
+ for key, value in data.items():
29
+ fixed_key = json.dumps(key, ensure_ascii=False).encode('utf8').decode()
30
+ fixed_value = json.dumps(value, ensure_ascii=False).encode('utf8').decode()
31
+ new_line = f'{fixed_key} = {fixed_value};\\n'
32
+ new_content += new_line
33
+
34
+ with open(file_path, 'w') as output_file:
35
+ output_file.write(new_content)
36
+ except:
37
+ return
38
+
39
+ file_extension = '.strings'
40
+ stringFiles = []
41
+
42
+ for root, _, files in os.walk(os.environ['BUILT_PRODUCTS_DIR'], followlinks=True):
43
+ for filename in files:
44
+ if filename.endswith(file_extension):
45
+ input_path = os.path.join(root, filename)
46
+ stringFiles.append(input_path)
47
+
48
+ # create a thread pool
49
+ with ThreadPool() as pool:
50
+ pool.map(minify, stringFiles)
51
+ }.freeze
52
+
53
+ def call(**options)
54
+ @options = options
55
+ before(options)
56
+
57
+ raise 'Path must be an xcodeproj' unless @options[:path].end_with?('.xcodeproj')
58
+ raise 'Path does not exist' unless File.exist?(@options[:path])
59
+
60
+ Sync do
61
+ project = Xcodeproj::Project.open(@options[:path])
62
+
63
+ project.targets.each do |target|
64
+ target.build_configurations.each do |config|
65
+ enable_user_script_sandboxing(config)
66
+ set_output_encoding(config)
67
+ end
68
+
69
+ add_run_script(target)
70
+ end
71
+
72
+ project.save
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def enable_user_script_sandboxing(config)
79
+ Logger.info "Enabling user script sandboxing for #{config.name}"
80
+ config.build_settings[ENABLE_USER_SCRIPT_SANDBOXING] = 'NO'
81
+ end
82
+
83
+ def set_output_encoding(config)
84
+ Logger.info "Setting output encoding for #{config.name}"
85
+ config.build_settings[STRINGS_FILE_OUTPUT_ENCODING] = STRINGS_FILE_OUTPUT_ENCODING_VALUE
86
+ end
87
+
88
+ def add_run_script(target)
89
+ phase = target.shell_script_build_phases.find { |item| item.name == SCRIPT_NAME }
90
+ return unless phase.nil?
91
+ Logger.info "Creating script '#{SCRIPT_NAME}'"
92
+ phase = target.new_shell_script_build_phase(SCRIPT_NAME)
93
+ phase.shell_script = SCRIPT_CONTENT
94
+ phase.shell_path = `which python3`.strip
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,116 @@
1
+ require 'dry/cli'
2
+ require 'xcodeproj'
3
+
4
+ module EmergeCLI
5
+ module Commands
6
+ module Autofixes
7
+ class StripBinarySymbols < EmergeCLI::Commands::GlobalOptions
8
+ desc 'Strip binary symbols from the app'
9
+
10
+ option :path, type: :string, required: true, desc: 'Path to the xcarchive'
11
+
12
+ # Constants
13
+ SCRIPT_NAME = 'EmergeTools Strip Binary Symbols'.freeze
14
+ ENABLE_USER_SCRIPT_SANDBOXING = 'ENABLE_USER_SCRIPT_SANDBOXING'.freeze
15
+ INPUT_FILE = '${DWARF_DSYM_FOLDER_PATH}/${EXECUTABLE_NAME}.app.dSYM/' \
16
+ 'Contents/Resources/DWARF/${EXECUTABLE_NAME}'.freeze
17
+ SCRIPT_CONTENT = %{#!/bin/bash
18
+ set -e
19
+
20
+ echo "Starting the symbol stripping process..."
21
+
22
+ if [ "Release" = "$\{CONFIGURATION\}" ]; then
23
+ echo "Configuration is Release."
24
+
25
+ # Path to the app directory
26
+ APP_DIR_PATH="$\{BUILT_PRODUCTS_DIR\}/$\{EXECUTABLE_FOLDER_PATH\}"
27
+ echo "App directory path: $\{APP_DIR_PATH\}"
28
+
29
+ # Strip main binary
30
+ echo "Stripping main binary: $\{APP_DIR_PATH\}/$\{EXECUTABLE_NAME\}"
31
+ strip -rSTx "$\{APP_DIR_PATH\}/$\{EXECUTABLE_NAME\}"
32
+ if [ $? -eq 0 ]; then
33
+ echo "Successfully stripped main binary."
34
+ else
35
+ echo "Failed to strip main binary." >&2
36
+ fi
37
+
38
+ # Path to the Frameworks directory
39
+ APP_FRAMEWORKS_DIR="$\{APP_DIR_PATH\}/Frameworks"
40
+ echo "Frameworks directory path: $\{APP_FRAMEWORKS_DIR\}"
41
+
42
+ # Strip symbols from frameworks, if Frameworks/ exists at all
43
+ # ... as long as the framework is NOT signed by Apple
44
+ if [ -d "$\{APP_FRAMEWORKS_DIR\}" ]; then
45
+ echo "Frameworks directory exists. Proceeding to strip symbols from frameworks."
46
+ find "$\{APP_FRAMEWORKS_DIR\}" -type f -perm +111 -maxdepth 2 -mindepth 2 -exec bash -c '
47
+ codesign -v -R="anchor apple" "\{\}" &> /dev/null ||
48
+ (
49
+ echo "Stripping \{\}" &&
50
+ if [ -w "\{\}" ]; then
51
+ strip -rSTx "\{\}"
52
+ if [ $? -eq 0 ]; then
53
+ echo "Successfully stripped \{\}"
54
+ else
55
+ echo "Failed to strip \{\}" >&2
56
+ fi
57
+ else
58
+ echo "Warning: No write permission for \{\}"
59
+ fi
60
+ )
61
+ ' \\;
62
+ if [ $? -eq 0 ]; then
63
+ echo "Successfully stripped symbols from frameworks."
64
+ else
65
+ echo "Failed to strip symbols from some frameworks." >&2
66
+ fi
67
+ else
68
+ echo "Frameworks directory does not exist. Skipping framework stripping."
69
+ fi
70
+ else
71
+ echo "Configuration is not Release. Skipping symbol stripping."
72
+ fi
73
+
74
+ echo "Symbol stripping process completed."}.freeze
75
+
76
+ def call(**options)
77
+ @options = options
78
+ before(options)
79
+
80
+ raise 'Path must be an xcodeproj' unless @options[:path].end_with?('.xcodeproj')
81
+ raise 'Path does not exist' unless File.exist?(@options[:path])
82
+
83
+ Sync do
84
+ project = Xcodeproj::Project.open(@options[:path])
85
+
86
+ project.targets.each do |target|
87
+ target.build_configurations.each do |config|
88
+ enable_user_script_sandboxing(config)
89
+ end
90
+
91
+ add_run_script(target)
92
+ end
93
+
94
+ project.save
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def enable_user_script_sandboxing(config)
101
+ Logger.info "Enabling user script sandboxing for #{config.name}"
102
+ config.build_settings[ENABLE_USER_SCRIPT_SANDBOXING] = 'NO'
103
+ end
104
+
105
+ def add_run_script(target)
106
+ phase = target.shell_script_build_phases.find { |item| item.name == SCRIPT_NAME }
107
+ return unless phase.nil?
108
+ Logger.info "Creating script '#{SCRIPT_NAME}'"
109
+ phase = target.new_shell_script_build_phase(SCRIPT_NAME)
110
+ phase.shell_script = SCRIPT_CONTENT
111
+ phase.input_paths = [INPUT_FILE]
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,107 @@
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 DownloadAndInstall < EmergeCLI::Commands::GlobalOptions
10
+ desc 'Download build from Build Distribution'
11
+
12
+ option :api_token, type: :string, required: false,
13
+ desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
14
+ option :build_id, type: :string, required: true, desc: 'Build ID to download'
15
+ option :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
16
+ option :device_id, type: :string, required: false, desc: 'Device id to install the build'
17
+ option :output, type: :string, required: false, desc: 'Output path for the downloaded build'
18
+
19
+ def initialize(network: nil)
20
+ @network = network
21
+ end
22
+
23
+ def call(**options)
24
+ @options = options
25
+ before(options)
26
+
27
+ Sync do
28
+ api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
29
+ raise 'API token is required' unless api_token
30
+
31
+ raise 'Build ID is required' unless @options[:build_id]
32
+
33
+ begin
34
+ @network ||= EmergeCLI::Network.new(api_token:)
35
+
36
+ Logger.info 'Getting build URL...'
37
+ request = get_build_url(@options[:build_id])
38
+ response = parse_response(request)
39
+
40
+ platform = response['platform']
41
+ download_url = response['downloadUrl']
42
+
43
+ extension = platform == 'ios' ? 'ipa' : 'apk'
44
+ Logger.info 'Downloading build...'
45
+ output_name = @options[:output] || "#{@options[:build_id]}.#{extension}"
46
+ `curl --progress-bar -L '#{download_url}' -o #{output_name} `
47
+ Logger.info "✅ Build downloaded to #{output_name}"
48
+
49
+ if @options[:install]
50
+ install_ios_build(output_name) if platform == 'ios'
51
+ install_android_build(output_name) if platform == 'android'
52
+ end
53
+ rescue StandardError => e
54
+ Logger.error "Failed to download build: #{e.message}"
55
+ Logger.error 'Check your parameters and try again'
56
+ raise e
57
+ ensure
58
+ @network&.close
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def get_build_url(build_id)
66
+ @network.get(
67
+ path: '/distribution/downloadUrl',
68
+ max_retries: 3,
69
+ query: {
70
+ buildId: build_id
71
+ }
72
+ )
73
+ end
74
+
75
+ def parse_response(response)
76
+ case response.status
77
+ when 200
78
+ JSON.parse(response.read)
79
+ when 400
80
+ error_message = JSON.parse(response.read)['errorMessage']
81
+ raise "Invalid parameters: #{error_message}"
82
+ when 401, 403
83
+ raise 'Invalid API token'
84
+ else
85
+ raise "Getting build failed with status #{response.status}"
86
+ end
87
+ end
88
+
89
+ def install_ios_build(build_path)
90
+ command = "xcrun devicectl device install app -d #{@options[:device_id]} #{build_path}"
91
+ Logger.debug "Running command: #{command}"
92
+ `#{command}`
93
+
94
+ Logger.info '✅ Build installed'
95
+ end
96
+
97
+ def install_android_build(build_path)
98
+ command = "adb -s #{@options[:device_id]} install #{build_path}"
99
+ Logger.debug "Running command: #{command}"
100
+ `#{command}`
101
+
102
+ Logger.info '✅ Build installed'
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,164 @@
1
+ require 'dry/cli'
2
+ require 'cfpropertylist'
3
+ require 'zip'
4
+ require 'rbconfig'
5
+
6
+ module EmergeCLI
7
+ module Commands
8
+ module BuildDistribution
9
+ class ValidateApp < EmergeCLI::Commands::GlobalOptions
10
+ desc 'Validate app for build distribution'
11
+
12
+ option :path, type: :string, required: true, desc: 'Path to the xcarchive, IPA or APK to validate'
13
+
14
+ # Constants
15
+ PLIST_START = '<plist'.freeze
16
+ PLIST_STOP = '</plist>'.freeze
17
+
18
+ UTF8_ENCODING = 'UTF-8'.freeze
19
+ STRING_FORMAT = 'binary'.freeze
20
+ EMPTY_STRING = ''.freeze
21
+
22
+ EXPECTED_ABI = 'arm64-v8a'.freeze
23
+
24
+ def call(**options)
25
+ @options = options
26
+ before(options)
27
+
28
+ Sync do
29
+ file_extension = File.extname(@options[:path])
30
+ case file_extension
31
+ when '.xcarchive'
32
+ handle_xcarchive
33
+ when '.ipa'
34
+ handle_ipa
35
+ when '.app'
36
+ handle_app
37
+ when '.apk'
38
+ handle_apk
39
+ else
40
+ raise "Unknown file extension: #{file_extension}"
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def handle_xcarchive
48
+ raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
49
+
50
+ app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
51
+ run_codesign_check(app_path)
52
+ read_provisioning_profile(app_path)
53
+ end
54
+
55
+ def handle_ipa
56
+ raise 'Path must be an IPA' unless @options[:path].end_with?('.ipa')
57
+
58
+ Dir.mktmpdir do |tmp_dir|
59
+ Zip::File.open(@options[:path]) do |zip_file|
60
+ zip_file.each do |entry|
61
+ entry.extract(File.join(tmp_dir, entry.name))
62
+ end
63
+ end
64
+
65
+ app_path = File.join(tmp_dir, 'Payload/*.app')
66
+ app_path = Dir.glob(app_path).first
67
+ run_codesign_check(app_path)
68
+ read_provisioning_profile(app_path)
69
+ end
70
+ end
71
+
72
+ def handle_app
73
+ raise 'Path must be an app' unless @options[:path].end_with?('.app')
74
+
75
+ app_path = @options[:path]
76
+ run_codesign_check(app_path)
77
+ read_provisioning_profile(app_path)
78
+ end
79
+
80
+ def handle_apk
81
+ raise 'Path must be an APK' unless @options[:path].end_with?('.apk')
82
+
83
+ apk_path = @options[:path]
84
+ check_supported_abis(apk_path)
85
+ end
86
+
87
+ def run_codesign_check(app_path)
88
+ unless RbConfig::CONFIG['host_os'] =~ /darwin/i
89
+ Logger.info 'Skipping codesign check on non-macOS platform'
90
+ return
91
+ end
92
+
93
+ command = "codesign -dvvv '#{app_path}'"
94
+ Logger.debug command
95
+ stdout, _, status = Open3.capture3(command)
96
+ Logger.debug stdout
97
+ raise '❌ Codesign check failed' unless status.success?
98
+
99
+ Logger.info '✅ Codesign check passed'
100
+ end
101
+
102
+ def read_provisioning_profile(app_path)
103
+ entitlements_path = File.join(app_path, 'embedded.mobileprovision')
104
+ raise '❌ Entitlements file not found' unless File.exist?(entitlements_path)
105
+
106
+ content = File.read(entitlements_path)
107
+ lines = content.lines
108
+
109
+ buffer = ''
110
+ inside_plist = false
111
+ lines.each do |line|
112
+ inside_plist = true if line.include? PLIST_START
113
+ if inside_plist
114
+ buffer << line
115
+ break if line.include? PLIST_STOP
116
+ end
117
+ end
118
+
119
+ encoded_plist = buffer.encode(UTF8_ENCODING, STRING_FORMAT, invalid: :replace, undef: :replace,
120
+ replace: EMPTY_STRING)
121
+ encoded_plist = encoded_plist.sub(/#{PLIST_STOP}.+/, PLIST_STOP)
122
+
123
+ plist = CFPropertyList::List.new(data: encoded_plist)
124
+ parsed_data = CFPropertyList.native_types(plist.value)
125
+
126
+ expiration_date = parsed_data['ExpirationDate']
127
+ if expiration_date > Time.now
128
+ Logger.info '✅ Provisioning profile hasn\'t expired'
129
+ else
130
+ Logger.info "❌ Provisioning profile is expired. Expiration date: #{expiration_date}"
131
+ end
132
+
133
+ provisions_all_devices = parsed_data['ProvisionsAllDevices']
134
+ if provisions_all_devices
135
+ Logger.info 'Provisioning profile supports all devices (likely an enterprise profile)'
136
+ else
137
+ devices = parsed_data['ProvisionedDevices']
138
+ Logger.info 'Provisioning profile does not support all devices (likely a development profile).'
139
+ Logger.info "Devices: #{devices.inspect}"
140
+ end
141
+ end
142
+
143
+ def check_supported_abis(apk_path)
144
+ abis = []
145
+
146
+ Zip::File.open(apk_path) do |zip_file|
147
+ zip_file.each do |entry|
148
+ if entry.name.start_with?('lib/') && entry.name.count('/') == 2
149
+ abi = entry.name.split('/')[1]
150
+ abis << abi unless abis.include?(abi)
151
+ end
152
+ end
153
+ end
154
+
155
+ unless abis.include?(EXPECTED_ABI)
156
+ raise "APK does not support #{EXPECTED_ABI} architecture, found: #{abis.join(', ')}"
157
+ end
158
+
159
+ Logger.info "✅ APK supports #{EXPECTED_ABI} architecture"
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,77 @@
1
+ require 'dry/cli'
2
+
3
+ module EmergeCLI
4
+ module Commands
5
+ class DownloadOrderFiles < EmergeCLI::Commands::GlobalOptions
6
+ desc 'Download order files from Emerge'
7
+
8
+ option :bundle_id, type: :string, required: true, desc: 'Bundle identifier to download order files for'
9
+
10
+ option :api_token, type: :string, required: false,
11
+ desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
12
+
13
+ option :app_version, type: :string, required: true,
14
+ desc: 'App version to download order files for'
15
+
16
+ option :unzip, type: :boolean, required: false,
17
+ desc: 'Unzip the order file after downloading'
18
+
19
+ option :output, type: :string, required: false,
20
+ desc: 'Output name for the order file, defaults to bundle_id-app_version.gz'
21
+
22
+ EMERGE_ORDER_FILE_URL = 'order-files-prod.emergetools.com'.freeze
23
+
24
+ def initialize(network: nil)
25
+ @network = network
26
+ end
27
+
28
+ def call(**options)
29
+ @options = options
30
+ before(options)
31
+
32
+ begin
33
+ api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
34
+ raise 'API token is required' unless api_token
35
+
36
+ raise 'Bundle ID is required' unless @options[:bundle_id]
37
+ raise 'App version is required' unless @options[:app_version]
38
+
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')
42
+
43
+ Sync do
44
+ request = get_order_file(options[:bundle_id], options[:app_version])
45
+ response = request.read
46
+
47
+ File.write(output_name, response)
48
+
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)
53
+ end
54
+ end
55
+
56
+ Logger.info 'Order file downloaded successfully'
57
+ 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
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def get_order_file(bundle_id, app_version)
70
+ @network.get(
71
+ path: "/#{bundle_id}/#{app_version}",
72
+ max_retries: 0
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,55 @@
1
+ require 'dry/cli'
2
+ require 'cfpropertylist'
3
+
4
+ module EmergeCLI
5
+ module Commands
6
+ class ValidateLinkmaps < EmergeCLI::Commands::GlobalOptions
7
+ desc 'Validate linkmaps in xcarchive'
8
+
9
+ option :path, type: :string, required: true, desc: 'Path to the xcarchive to validate'
10
+
11
+ def initialize(network: nil)
12
+ @network = network
13
+ end
14
+
15
+ def call(**options)
16
+ @options = options
17
+ before(options)
18
+
19
+ Sync do
20
+ executable_name = get_executable_name
21
+ raise 'Executable not found' if executable_name.nil?
22
+
23
+ Logger.info "Using executable: #{executable_name}"
24
+
25
+ linkmaps_path = File.join(@options[:path], 'Linkmaps')
26
+ raise 'Linkmaps folder not found' unless File.directory?(linkmaps_path)
27
+
28
+ linkmaps = Dir.glob("#{linkmaps_path}/*.txt")
29
+ raise 'No linkmaps found' if linkmaps.empty?
30
+
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?
35
+
36
+ Logger.info "✅ Found linkmaps for #{executable_name}"
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def get_executable_name
43
+ raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
44
+
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)
50
+
51
+ parsed_data['CFBundleExecutable']
52
+ end
53
+ end
54
+ end
55
+ end