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 +4 -4
- data/lib/commands/autofixes/exported_symbols.rb +62 -0
- data/lib/commands/autofixes/minify_strings.rb +99 -0
- data/lib/commands/autofixes/strip_binary_symbols.rb +116 -0
- data/lib/commands/build_distribution/download_and_install.rb +139 -0
- data/lib/commands/build_distribution/validate_app.rb +164 -0
- data/lib/commands/order_files/validate_xcode_project.rb +71 -0
- data/lib/commands/upload/build.rb +140 -0
- data/lib/emerge_cli.rb +24 -0
- data/lib/reaper/ast_parser.rb +17 -16
- data/lib/reaper/code_deleter.rb +3 -1
- data/lib/utils/environment.rb +7 -0
- data/lib/utils/git.rb +8 -0
- data/lib/utils/network.rb +2 -2
- data/lib/utils/xcode_device_manager.rb +158 -0
- data/lib/utils/xcode_physical_device.rb +108 -0
- data/lib/utils/xcode_simulator.rb +114 -0
- data/lib/version.rb +1 -1
- data/parsers/libtree-sitter-objc-darwin-arm64.dylib +0 -0
- data/parsers/libtree-sitter-objc-linux-x86_64.so +0 -0
- metadata +32 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cce2cb7764c86a70569537aab8d02bd3136df5c4b25b2421164bf0e28256623
|
4
|
+
data.tar.gz: e4cce6ab186fd05a9910acc99cb279d60e7799f36273f45d6496523e785ede62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|