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 +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 +107 -0
- data/lib/commands/build_distribution/validate_app.rb +164 -0
- data/lib/commands/order_files/download_order_files.rb +77 -0
- data/lib/commands/order_files/validate_linkmaps.rb +55 -0
- data/lib/commands/order_files/validate_xcode_project.rb +71 -0
- data/lib/commands/snapshots/validate_app.rb +64 -0
- data/lib/commands/upload/build.rb +140 -0
- data/lib/emerge_cli.rb +33 -0
- data/lib/reaper/ast_parser.rb +17 -16
- data/lib/reaper/code_deleter.rb +3 -1
- data/lib/utils/git.rb +21 -1
- data/lib/utils/macho_parser.rb +325 -0
- data/lib/utils/network.rb +12 -10
- 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 +66 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ebd308782a251e11a4d515c0728b572456ed730ef2376f634939fec6ace820e
|
4
|
+
data.tar.gz: d476865caa4a62c32d9b258ec6fbfb474cb67c7bdf05c3ef1548ff860a1a9697
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|