Skip to content

Commit

Permalink
Command naming consistency
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-e committed Jan 29, 2025
1 parent 8cb52d4 commit 41ea374
Show file tree
Hide file tree
Showing 11 changed files with 476 additions and 464 deletions.
141 changes: 141 additions & 0 deletions lib/commands/build/distribution/install.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
require 'dry/cli'
require 'cfpropertylist'
require 'zip'
require 'rbconfig'
require 'tmpdir'

module EmergeCLI
module Commands
module Build
module Distribution
class Install < EmergeCLI::Commands::GlobalOptions
desc 'Download and install a build from Build Distribution'

option :api_token, type: :string, required: false,
desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
option :build_id, type: :string, required: true, desc: 'Build ID to download'
option :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
option :device_id, type: :string, desc: 'Specific device ID to target'
option :device_type, type: :string, enum: %w[virtual physical any], default: 'any',
desc: 'Type of device to target (virtual/physical/any)'
option :output, type: :string, required: false, desc: 'Output path for the downloaded build'

def initialize(network: nil)
@network = network
end

def call(**options)
@options = options
before(options)

Sync do
api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
raise 'API token is required' unless api_token

raise 'Build ID is required' unless @options[:build_id]

output_name = nil
app_id = nil

begin
@network ||= EmergeCLI::Network.new(api_token:)

Logger.info 'Getting build URL...'
request = get_build_url(@options[:build_id])
response = parse_response(request)

platform = response['platform']
download_url = response['downloadUrl']
app_id = response['appId']

extension = platform == 'ios' ? 'ipa' : 'apk'
Logger.info 'Downloading build...'
output_name = @options[:output] || "#{@options[:build_id]}.#{extension}"
`curl --progress-bar -L '#{download_url}' -o #{output_name} `
Logger.info "✅ Build downloaded to #{output_name}"
rescue StandardError => e
Logger.error "❌ Failed to download build: #{e.message}"
raise e
ensure
@network&.close
end

begin
if @options[:install] && !output_name.nil?
if platform == 'ios'
install_ios_build(output_name, app_id)
elsif platform == 'android'
install_android_build(output_name)
end
end
rescue StandardError => e
Logger.error "❌ Failed to install build: #{e.message}"
raise e
end
end
end

private

def get_build_url(build_id)
@network.get(
path: '/distribution/downloadUrl',
max_retries: 3,
query: {
buildId: build_id
}
)
end

def parse_response(response)
case response.status
when 200
JSON.parse(response.read)
when 400
error_message = JSON.parse(response.read)['errorMessage']
raise "Invalid parameters: #{error_message}"
when 401, 403
raise 'Invalid API token'
else
raise "Getting build failed with status #{response.status}"
end
end

def install_ios_build(build_path, app_id)
device_type = case @options[:device_type]
when 'simulator'
XcodeDeviceManager::DeviceType::VIRTUAL
when 'physical'
XcodeDeviceManager::DeviceType::PHYSICAL
else
XcodeDeviceManager::DeviceType::ANY
end

device_manager = XcodeDeviceManager.new
device = if @options[:device_id]
device_manager.find_device_by_id(@options[:device_id])
else
device_manager.find_device_by_type(device_type, build_path)
end

Logger.info "Installing build on #{device.device_id}"
device.install_app(build_path)
Logger.info '✅ Build installed'

Logger.info "Launching app #{app_id}..."
device.launch_app(app_id)
Logger.info '✅ Build launched'
end

def install_android_build(build_path)
command = "adb -s #{@options[:device_id]} install #{build_path}"
Logger.debug "Running command: #{command}"
`#{command}`

Logger.info '✅ Build installed'
end
end
end
end
end
end
166 changes: 166 additions & 0 deletions lib/commands/build/distribution/validate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
require 'dry/cli'
require 'cfpropertylist'
require 'zip'
require 'rbconfig'

module EmergeCLI
module Commands
module Build
module Distribution
class ValidateApp < EmergeCLI::Commands::GlobalOptions
desc 'Validate app for build distribution'

option :path, type: :string, required: true, desc: 'Path to the xcarchive, IPA or APK to validate'

# Constants
PLIST_START = '<plist'.freeze
PLIST_STOP = '</plist>'.freeze

UTF8_ENCODING = 'UTF-8'.freeze
STRING_FORMAT = 'binary'.freeze
EMPTY_STRING = ''.freeze

EXPECTED_ABI = 'arm64-v8a'.freeze

def call(**options)
@options = options
before(options)

Sync do
file_extension = File.extname(@options[:path])
case file_extension
when '.xcarchive'
handle_xcarchive
when '.ipa'
handle_ipa
when '.app'
handle_app
when '.apk'
handle_apk
else
raise "Unknown file extension: #{file_extension}"
end
end
end

private

def handle_xcarchive
raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')

app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
run_codesign_check(app_path)
read_provisioning_profile(app_path)
end

def handle_ipa
raise 'Path must be an IPA' unless @options[:path].end_with?('.ipa')

Dir.mktmpdir do |tmp_dir|
Zip::File.open(@options[:path]) do |zip_file|
zip_file.each do |entry|
entry.extract(File.join(tmp_dir, entry.name))
end
end

app_path = File.join(tmp_dir, 'Payload/*.app')
app_path = Dir.glob(app_path).first
run_codesign_check(app_path)
read_provisioning_profile(app_path)
end
end

def handle_app
raise 'Path must be an app' unless @options[:path].end_with?('.app')

app_path = @options[:path]
run_codesign_check(app_path)
read_provisioning_profile(app_path)
end

def handle_apk
raise 'Path must be an APK' unless @options[:path].end_with?('.apk')

apk_path = @options[:path]
check_supported_abis(apk_path)
end

def run_codesign_check(app_path)
unless RbConfig::CONFIG['host_os'] =~ /darwin/i
Logger.info 'Skipping codesign check on non-macOS platform'
return
end

command = "codesign -dvvv '#{app_path}'"
Logger.debug command
stdout, _, status = Open3.capture3(command)
Logger.debug stdout
raise '❌ Codesign check failed' unless status.success?

Logger.info '✅ Codesign check passed'
end

def read_provisioning_profile(app_path)
entitlements_path = File.join(app_path, 'embedded.mobileprovision')
raise '❌ Entitlements file not found' unless File.exist?(entitlements_path)

content = File.read(entitlements_path)
lines = content.lines

buffer = ''
inside_plist = false
lines.each do |line|
inside_plist = true if line.include? PLIST_START
if inside_plist
buffer << line
break if line.include? PLIST_STOP
end
end

encoded_plist = buffer.encode(UTF8_ENCODING, STRING_FORMAT, invalid: :replace, undef: :replace,
replace: EMPTY_STRING)
encoded_plist = encoded_plist.sub(/#{PLIST_STOP}.+/, PLIST_STOP)

plist = CFPropertyList::List.new(data: encoded_plist)
parsed_data = CFPropertyList.native_types(plist.value)

expiration_date = parsed_data['ExpirationDate']
if expiration_date > Time.now
Logger.info '✅ Provisioning profile hasn\'t expired'
else
Logger.info "❌ Provisioning profile is expired. Expiration date: #{expiration_date}"
end

provisions_all_devices = parsed_data['ProvisionsAllDevices']
if provisions_all_devices
Logger.info 'Provisioning profile supports all devices (likely an enterprise profile)'
else
devices = parsed_data['ProvisionedDevices']
Logger.info 'Provisioning profile does not support all devices (likely a development profile).'
Logger.info "Devices: #{devices.inspect}"
end
end

def check_supported_abis(apk_path)
abis = []

Zip::File.open(apk_path) do |zip_file|
zip_file.each do |entry|
if entry.name.start_with?('lib/') && entry.name.count('/') == 2
abi = entry.name.split('/')[1]
abis << abi unless abis.include?(abi)
end
end
end

unless abis.include?(EXPECTED_ABI)
raise "APK does not support #{EXPECTED_ABI} architecture, found: #{abis.join(', ')}"
end

Logger.info "✅ APK supports #{EXPECTED_ABI} architecture"
end
end
end
end
end
end
Loading

0 comments on commit 41ea374

Please sign in to comment.