emerge 0.4.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: e4ada02f9680b03cfcf2d4350d0aa719ae8c1a0399e7783b468b341e2eac5143
4
- data.tar.gz: 7dfc873c8ba7cb9dd145b7ae181cdcadbe494741ac6f241fcdd0cb48f3c88526
3
+ metadata.gz: 1ebd308782a251e11a4d515c0728b572456ed730ef2376f634939fec6ace820e
4
+ data.tar.gz: d476865caa4a62c32d9b258ec6fbfb474cb67c7bdf05c3ef1548ff860a1a9697
5
5
  SHA512:
6
- metadata.gz: 4ae1796a1d262846e12bf5b1f64301166209b0ad31f4981effb118a4c091d0f36c5396e169c881b8baea1f5fc93e18f8aa452eb1e05775eeab79b615e6b38370
7
- data.tar.gz: ff14aac430f27b95be0d6267839d967e9869b31090c669d68c0e8fefb34dd6e3468bc627080f36ce27aaa1288d73e2698c5800614035f5218d7d71a2a5182aef
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,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
@@ -0,0 +1,140 @@
1
+ require 'dry/cli'
2
+ require 'json'
3
+ require 'uri'
4
+ require 'async'
5
+ require 'async/barrier'
6
+ require 'async/semaphore'
7
+ require 'async/http/internet/instance'
8
+
9
+ module EmergeCLI
10
+ module Commands
11
+ module Upload
12
+ class Build < EmergeCLI::Commands::GlobalOptions
13
+ desc 'Upload a build to Emerge'
14
+
15
+ option :path, type: :string, required: true, desc: 'Path to the build artifact'
16
+
17
+ # Optional options
18
+ option :api_token, type: :string, required: false,
19
+ desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
20
+ option :sha, type: :string, required: false, desc: 'SHA of the commit'
21
+ option :branch, type: :string, required: false, desc: 'Branch name'
22
+ option :repo_name, type: :string, required: false, desc: 'Repository name'
23
+ option :base_sha, type: :string, required: false, desc: 'Base SHA'
24
+ option :previous_sha, type: :string, required: false, desc: 'Previous SHA'
25
+ option :pr_number, type: :string, required: false, desc: 'PR number'
26
+
27
+ def initialize(network: nil, git_info_provider: nil)
28
+ @network = network
29
+ @git_info_provider = git_info_provider
30
+ end
31
+
32
+ def call(**options)
33
+ @options = options
34
+ @profiler = EmergeCLI::Profiler.new(enabled: options[:profile])
35
+ before(options)
36
+
37
+ start_time = Time.now
38
+
39
+ file_path = options[:path]
40
+ file_exists = File.exist?(file_path)
41
+ raise "File not found at path: #{file_path}" unless file_exists
42
+
43
+ file_extension = File.extname(file_path)
44
+ raise "Unsupported file type: #{file_extension}" unless ['.ipa', '.apk', '.aab',
45
+ '.zip'].include?(file_extension)
46
+
47
+ api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
48
+ raise 'API token is required and cannot be blank' if api_token.nil? || api_token.strip.empty?
49
+
50
+ @network ||= EmergeCLI::Network.new(api_token:)
51
+ @git_info_provider ||= GitInfoProvider.new
52
+
53
+ Sync do
54
+ upload_url, upload_id = fetch_upload_url
55
+
56
+ file_size = File.size(file_path)
57
+ Logger.info("Uploading file... (#{file_size} bytes)")
58
+
59
+ File.open(file_path, 'rb') do |file|
60
+ headers = {
61
+ 'Content-Type' => 'application/zip',
62
+ 'Content-Length' => file_size.to_s
63
+ }
64
+
65
+ response = @network.put(
66
+ path: upload_url,
67
+ body: file.read,
68
+ headers: headers
69
+ )
70
+
71
+ unless response.status == 200
72
+ Logger.error("Upload failed with status #{response.status}")
73
+ Logger.error("Response body: #{response.body}")
74
+ raise "Uploading file failed with status #{response.status}"
75
+ end
76
+ end
77
+
78
+ Logger.info('Upload complete successfully!')
79
+ Logger.info "Time taken: #{(Time.now - start_time).round(2)} seconds"
80
+ Logger.info("✅ You can view the build analysis at https://emergetools.com/build/#{upload_id}")
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def fetch_upload_url
87
+ git_result = @git_info_provider.fetch_git_info
88
+ sha = @options[:sha] || git_result.sha
89
+ branch = @options[:branch] || git_result.branch
90
+ base_sha = @options[:base_sha] || git_result.base_sha
91
+ previous_sha = @options[:previous_sha] || git_result.previous_sha
92
+ pr_number = @options[:pr_number] || git_result.pr_number
93
+
94
+ # TODO: Make optional
95
+ raise 'SHA is required' unless sha
96
+ raise 'Branch is required' unless branch
97
+
98
+ payload = {
99
+ sha:,
100
+ branch:,
101
+ repo_name: @options[:repo_name],
102
+ # Optional
103
+ base_sha:,
104
+ previous_sha:,
105
+ pr_number: pr_number&.to_s
106
+ }.compact
107
+
108
+ upload_response = @network.post(
109
+ path: '/upload',
110
+ body: payload,
111
+ headers: { 'Content-Type' => 'application/json' }
112
+ )
113
+ upload_json = parse_response(upload_response)
114
+ upload_id = upload_json.fetch('upload_id')
115
+ upload_url = upload_json.fetch('uploadURL')
116
+ Logger.debug("Got upload ID: #{upload_id}")
117
+
118
+ warning = upload_json['warning']
119
+ Logger.warn(warning) if warning
120
+
121
+ [upload_url, upload_id]
122
+ end
123
+
124
+ def parse_response(response)
125
+ case response.status
126
+ when 200
127
+ JSON.parse(response.read)
128
+ when 400
129
+ error_message = JSON.parse(response.read)['errorMessage']
130
+ raise "Invalid parameters: #{error_message}"
131
+ when 401, 403
132
+ raise 'Invalid API token'
133
+ else
134
+ raise "Creating upload failed with status #{response.status}"
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
data/lib/emerge_cli.rb CHANGED
@@ -13,6 +13,13 @@ require_relative 'commands/reaper/reaper'
13
13
  require_relative 'commands/snapshots/validate_app'
14
14
  require_relative 'commands/order_files/download_order_files'
15
15
  require_relative 'commands/order_files/validate_linkmaps'
16
+ require_relative 'commands/order_files/validate_xcode_project'
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'
16
23
 
17
24
  require_relative 'reaper/ast_parser'
18
25
  require_relative 'reaper/code_deleter'
@@ -34,6 +41,7 @@ module EmergeCLI
34
41
  extend Dry::CLI::Registry
35
42
 
36
43
  register 'upload', aliases: ['u'] do |prefix|
44
+ prefix.register 'build', Commands::Upload::Build
37
45
  prefix.register 'snapshots', Commands::Upload::Snapshots
38
46
  end
39
47
 
@@ -55,6 +63,18 @@ module EmergeCLI
55
63
  register 'order-files' do |prefix|
56
64
  prefix.register 'download', Commands::DownloadOrderFiles
57
65
  prefix.register 'validate-linkmaps', Commands::ValidateLinkmaps
66
+ prefix.register 'validate-xcode-project', Commands::ValidateXcodeProject
67
+ end
68
+
69
+ register 'build-distribution' do |prefix|
70
+ prefix.register 'validate-app', Commands::BuildDistribution::ValidateApp
71
+ prefix.register 'install', Commands::BuildDistribution::DownloadAndInstall
72
+ end
73
+
74
+ register 'autofix' do |prefix|
75
+ prefix.register 'minify-strings', Commands::Autofixes::MinifyStrings
76
+ prefix.register 'strip-binary-symbols', Commands::Autofixes::StripBinarySymbols
77
+ prefix.register 'exported-symbols', Commands::Autofixes::ExportedSymbols
58
78
  end
59
79
  end
60
80
 
@@ -9,19 +9,22 @@ module EmergeCLI
9
9
  DECLARATION_NODE_TYPES = {
10
10
  'swift' => %i[class_declaration protocol_declaration],
11
11
  'kotlin' => %i[class_declaration protocol_declaration interface_declaration object_declaration],
12
- 'java' => %i[class_declaration protocol_declaration interface_declaration]
12
+ 'java' => %i[class_declaration protocol_declaration interface_declaration],
13
+ 'objc' => %i[class_declaration protocol_declaration class_implementation class_interface]
13
14
  }.freeze
14
15
 
15
16
  IDENTIFIER_NODE_TYPES = {
16
17
  'swift' => %i[simple_identifier qualified_name identifier type_identifier],
17
18
  'kotlin' => %i[simple_identifier qualified_name identifier type_identifier],
18
- 'java' => %i[simple_identifier qualified_name identifier type_identifier]
19
+ 'java' => %i[simple_identifier qualified_name identifier type_identifier],
20
+ 'objc' => %i[simple_identifier qualified_name identifier type_identifier]
19
21
  }.freeze
20
22
 
21
23
  COMMENT_AND_IMPORT_NODE_TYPES = {
22
24
  'swift' => %i[comment import_declaration],
23
25
  'kotlin' => %i[comment import_header],
24
- 'java' => %i[comment import_declaration]
26
+ 'java' => %i[comment import_declaration],
27
+ 'objc' => %i[comment import_declaration preproc_include]
25
28
  }.freeze
26
29
 
27
30
  attr_reader :parser, :language
@@ -52,17 +55,9 @@ module EmergeCLI
52
55
  extension = platform == 'darwin' ? 'dylib' : 'so'
53
56
  parser_file = "libtree-sitter-#{language}-#{platform}-#{arch}.#{extension}"
54
57
  parser_path = File.join('parsers', parser_file)
58
+ raise "No language grammar found for #{language}" unless File.exist?(parser_path)
55
59
 
56
- case language
57
- when 'swift'
58
- @parser.language = TreeSitter::Language.load('swift', parser_path)
59
- when 'kotlin'
60
- @parser.language = TreeSitter::Language.load('kotlin', parser_path)
61
- when 'java'
62
- @parser.language = TreeSitter::Language.load('java', parser_path)
63
- else
64
- raise "Unsupported language: #{language}"
65
- end
60
+ @parser.language = TreeSitter::Language.load(language, parser_path)
66
61
  end
67
62
 
68
63
  # Deletes a type from the given file contents.
@@ -127,6 +122,7 @@ module EmergeCLI
127
122
 
128
123
  while (node = nodes_to_process.shift)
129
124
  identifier_type = identifier_node_types.include?(node.type)
125
+ Logger.debug "Processing node: #{node.type} #{node_text(node)}"
130
126
  declaration_type = if node == tree.root_node
131
127
  false
132
128
  else
@@ -136,6 +132,11 @@ module EmergeCLI
136
132
  usages << { line: node.start_point.row, usage_type: 'declaration' }
137
133
  elsif identifier_type && node_text(node) == type_name
138
134
  usages << { line: node.start_point.row, usage_type: 'identifier' }
135
+ elsif node.type == :@implementation
136
+ next_sibling = node.next_named_sibling
137
+ if next_sibling.type == :identifier && node_text(next_sibling) == type_name
138
+ usages << { line: next_sibling.start_point.row, usage_type: 'declaration' }
139
+ end
139
140
  end
140
141
 
141
142
  node.each { |child| nodes_to_process.push(child) }
@@ -172,14 +173,14 @@ module EmergeCLI
172
173
 
173
174
  return file_contents if nodes_to_remove.empty?
174
175
 
175
- Logger.debug "Found #{nodes_to_remove.length} nodes to remove"
176
+ Logger.debug "Found #{nodes_to_remove.length} nodes to remove"
176
177
  remove_nodes_from_content(file_contents, nodes_to_remove)
177
178
  end
178
179
 
179
180
  private
180
181
 
181
182
  def remove_node(node, lines_to_remove)
182
- Logger.debug "Removing node: #{node.type}"
183
+ Logger.debug "Removing node: #{node.type}"
183
184
  start_position = node.start_point.row
184
185
  end_position = node.end_point.row
185
186
  lines_to_remove << { start: start_position, end: end_position }
@@ -287,7 +288,7 @@ module EmergeCLI
287
288
  when :navigation_expression # NetworkDebugger.printStats
288
289
  result = handle_navigation_expression(current)
289
290
  return result if result
290
- when :class_declaration, :function_declaration, :method_declaration
291
+ when :class_declaration, :function_declaration, :method_declaration, :@implementation
291
292
  Logger.debug "Reached structural element, stopping at: #{current.type}"
292
293
  break
293
294
  end
@@ -163,7 +163,8 @@ module EmergeCLI
163
163
  found_usages = []
164
164
  source_patterns = case @platform&.downcase
165
165
  when 'ios'
166
- { 'swift' => '**/*.swift' }
166
+ { 'swift' => '**/*.swift',
167
+ 'objc' => '**/*.{m,h}' }
167
168
  when 'android'
168
169
  {
169
170
  'kotlin' => '**/*.kt',
@@ -253,6 +254,7 @@ module EmergeCLI
253
254
  when '.swift' then 'swift'
254
255
  when '.kt' then 'kotlin'
255
256
  when '.java' then 'java'
257
+ when '.m', '.h' then 'objc'
256
258
  else
257
259
  raise "Unsupported file type for #{file_path}"
258
260
  end
data/lib/utils/git.rb CHANGED
@@ -3,6 +3,7 @@ require 'open3'
3
3
  module EmergeCLI
4
4
  module Git
5
5
  def self.branch
6
+ Logger.debug 'Getting current branch name'
6
7
  command = 'git rev-parse --abbrev-ref HEAD'
7
8
  Logger.debug command
8
9
  stdout, _, status = Open3.capture3(command)
@@ -39,6 +40,7 @@ module EmergeCLI
39
40
  end
40
41
 
41
42
  def self.sha
43
+ Logger.debug 'Getting current SHA'
42
44
  command = 'git rev-parse HEAD'
43
45
  Logger.debug command
44
46
  stdout, _, status = Open3.capture3(command)
@@ -46,6 +48,7 @@ module EmergeCLI
46
48
  end
47
49
 
48
50
  def self.base_sha
51
+ Logger.debug 'Getting base SHA'
49
52
  current_branch = branch
50
53
  remote_head = remote_head_branch
51
54
  return nil if current_branch.nil? || remote_head.nil?
@@ -59,6 +62,7 @@ module EmergeCLI
59
62
  end
60
63
 
61
64
  def self.previous_sha
65
+ Logger.debug 'Getting previous SHA'
62
66
  command = 'git rev-list --count HEAD'
63
67
  Logger.debug command
64
68
  count_stdout, _, count_status = Open3.capture3(command)
@@ -78,12 +82,14 @@ module EmergeCLI
78
82
  end
79
83
 
80
84
  def self.primary_remote
85
+ Logger.debug 'Getting primary remote'
81
86
  remote = remote()
82
87
  return nil if remote.nil?
83
88
  remote.include?('origin') ? 'origin' : remote.first
84
89
  end
85
90
 
86
91
  def self.remote_head_branch(remote = primary_remote)
92
+ Logger.debug 'Getting remote head branch'
87
93
  return nil if remote.nil?
88
94
  command = "git remote show #{remote}"
89
95
  Logger.debug command
@@ -98,6 +104,7 @@ module EmergeCLI
98
104
  end
99
105
 
100
106
  def self.remote_url(remote = primary_remote)
107
+ Logger.debug 'Getting remote URL'
101
108
  return nil if remote.nil?
102
109
  command = "git config --get remote.#{remote}.url"
103
110
  Logger.debug command
@@ -106,6 +113,7 @@ module EmergeCLI
106
113
  end
107
114
 
108
115
  def self.remote
116
+ Logger.debug 'Getting remote'
109
117
  command = 'git remote'
110
118
  Logger.debug command
111
119
  stdout, _, status = Open3.capture3(command)
data/lib/utils/network.rb CHANGED
@@ -17,8 +17,8 @@ module EmergeCLI
17
17
  @internet = Async::HTTP::Internet.new
18
18
  end
19
19
 
20
- def get(path:, headers: {}, max_retries: MAX_RETRIES)
21
- request(:get, path, nil, headers, nil, max_retries)
20
+ def get(path:, headers: {}, query: nil, max_retries: MAX_RETRIES)
21
+ request(:get, path, nil, headers, query, max_retries)
22
22
  end
23
23
 
24
24
  def post(path:, body:, headers: {}, query: nil, max_retries: MAX_RETRIES)
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module EmergeCLI
2
- VERSION = '0.4.0'.freeze
2
+ VERSION = '0.5.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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emerge Tools
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-17 00:00:00.000000000 Z
11
+ date: 2025-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -114,6 +114,20 @@ dependencies:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
116
  version: '1.9'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubyzip
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 2.3.0
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 2.3.0
117
131
  - !ruby/object:Gem::Dependency
118
132
  name: tty-prompt
119
133
  requirement: !ruby/object:Gem::Requirement
@@ -167,14 +181,21 @@ files:
167
181
  - CHANGELOG.md
168
182
  - README.md
169
183
  - exe/emerge
184
+ - lib/commands/autofixes/exported_symbols.rb
185
+ - lib/commands/autofixes/minify_strings.rb
186
+ - lib/commands/autofixes/strip_binary_symbols.rb
187
+ - lib/commands/build_distribution/download_and_install.rb
188
+ - lib/commands/build_distribution/validate_app.rb
170
189
  - lib/commands/config/orderfiles/orderfiles_ios.rb
171
190
  - lib/commands/config/snapshots/snapshots_ios.rb
172
191
  - lib/commands/global_options.rb
173
192
  - lib/commands/integrate/fastlane.rb
174
193
  - lib/commands/order_files/download_order_files.rb
175
194
  - lib/commands/order_files/validate_linkmaps.rb
195
+ - lib/commands/order_files/validate_xcode_project.rb
176
196
  - lib/commands/reaper/reaper.rb
177
197
  - lib/commands/snapshots/validate_app.rb
198
+ - lib/commands/upload/build.rb
178
199
  - lib/commands/upload/snapshots/client_libraries/default.rb
179
200
  - lib/commands/upload/snapshots/client_libraries/paparazzi.rb
180
201
  - lib/commands/upload/snapshots/client_libraries/roborazzi.rb
@@ -198,6 +219,8 @@ files:
198
219
  - parsers/libtree-sitter-java-linux-x86_64.so
199
220
  - parsers/libtree-sitter-kotlin-darwin-arm64.dylib
200
221
  - parsers/libtree-sitter-kotlin-linux-x86_64.so
222
+ - parsers/libtree-sitter-objc-darwin-arm64.dylib
223
+ - parsers/libtree-sitter-objc-linux-x86_64.so
201
224
  - parsers/libtree-sitter-swift-darwin-arm64.dylib
202
225
  - parsers/libtree-sitter-swift-linux-x86_64.so
203
226
  homepage: https://github.com/EmergeTools/emerge-cli
@@ -208,7 +231,7 @@ metadata:
208
231
  source_code_uri: https://github.com/EmergeTools/emerge-cli
209
232
  changelog_uri: https://github.com/EmergeTools/emerge-cli/blob/main/CHANGELOG.md
210
233
  rubygems_mfa_required: 'true'
211
- post_install_message:
234
+ post_install_message:
212
235
  rdoc_options: []
213
236
  require_paths:
214
237
  - lib
@@ -224,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
224
247
  version: '0'
225
248
  requirements: []
226
249
  rubygems_version: 3.5.11
227
- signing_key:
250
+ signing_key:
228
251
  specification_version: 4
229
252
  summary: Emerge CLI
230
253
  test_files: []