emerge 0.4.0 → 0.6.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: e4ada02f9680b03cfcf2d4350d0aa719ae8c1a0399e7783b468b341e2eac5143
4
- data.tar.gz: 7dfc873c8ba7cb9dd145b7ae181cdcadbe494741ac6f241fcdd0cb48f3c88526
3
+ metadata.gz: 0cce2cb7764c86a70569537aab8d02bd3136df5c4b25b2421164bf0e28256623
4
+ data.tar.gz: e4cce6ab186fd05a9910acc99cb279d60e7799f36273f45d6496523e785ede62
5
5
  SHA512:
6
- metadata.gz: 4ae1796a1d262846e12bf5b1f64301166209b0ad31f4981effb118a4c091d0f36c5396e169c881b8baea1f5fc93e18f8aa452eb1e05775eeab79b615e6b38370
7
- data.tar.gz: ff14aac430f27b95be0d6267839d967e9869b31090c669d68c0e8fefb34dd6e3468bc627080f36ce27aaa1288d73e2698c5800614035f5218d7d71a2a5182aef
6
+ metadata.gz: 5ebaf9ef828373305f07c597fb2968d2e046689d5954226210ba9ca164c919b146bc208c83adcd77acc77d7a535feeeb31c7cc0718944c05fb7990cbc7e1beee
7
+ data.tar.gz: 46e65589c521670cff6ff14106e96c33d9fb2aa0d8293b5521878f1e97a2aef59516a193a08c75b4371c1d83af15b2b98f56e8245bdd4c83c72c0565c3c6130a
@@ -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,139 @@
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
@@ -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,71 @@
1
+ require 'dry/cli'
2
+ require 'xcodeproj'
3
+
4
+ module EmergeCLI
5
+ module Commands
6
+ class ValidateXcodeProject < EmergeCLI::Commands::GlobalOptions
7
+ desc 'Validate xcodeproject for order files'
8
+
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)'
13
+
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
18
+
19
+ def call(**options)
20
+ @options = options
21
+ before(options)
22
+
23
+ raise 'Path must be an xcodeproject' unless @options[:path].end_with?('.xcodeproj')
24
+ raise 'Path does not exist' unless File.exist?(@options[:path])
25
+
26
+ @options[:build_configuration] ||= 'Release'
27
+
28
+ Sync do
29
+ project = Xcodeproj::Project.open(@options[:path])
30
+
31
+ validate_xcproj(project)
32
+ end
33
+ end
34
+
35
+ private
36
+
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'
41
+
42
+ target.build_configurations.each do |config|
43
+ next if config.name != @options[:build_configuration]
44
+ validate_target_config(target, config)
45
+ end
46
+ end
47
+ end
48
+
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 \
58
+ setting it to '#{PATH_TO_LINKMAP}'"
59
+ end
60
+
61
+ if has_error
62
+ Logger.error "❌ Target '#{target.name}' has errors, this means \
63
+ 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"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end