665 lines
4.7 MiB
Ruby
665 lines
4.7 MiB
Ruby
#!/usr/bin/env ruby
|
|||
|
|
# ----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
|
# AppLovin Quality Service Setup Script for iOS
|
||
|
|
# (c) 2023 AppLovin. All Rights Reserved
|
||
|
|
#
|
||
|
|
# Syntax:
|
||
|
|
# ruby AppLovinQualityServiceSetup-xyz.rb <command> <options>
|
||
|
|
#
|
||
|
|
# Commands:
|
||
|
|
# install - Installs AppLovinQualityService onto an iOS target
|
||
|
|
# uninstall - Uninstalls AppLovinQualityService from a target
|
||
|
|
# update - Updates the AppLovinQualityService pod to the latest version, or to specific version if version number follows.
|
||
|
|
# This command should only be used from within the context of an Xcode build phase
|
||
|
|
#
|
||
|
|
# Options:
|
||
|
|
# -targetid <target id> - installs on a specific target identified by its ID as appears in the xcodeproj file (applicable to "install" and "uninstall")
|
||
|
|
# -targetname <target name> - installs on a specific target identified by its name (applicable to "install" and "uninstall")
|
||
|
|
# -localframeworks <framework names comma-separated without spaces> - instructs AppLovinQualityService Plugin to analyze these frameworks looking for SDKs
|
||
|
|
# -version <version> - installs a specific version and blocks automatic future updates (applicable to "install")
|
||
|
|
#
|
||
|
|
# Examples:
|
||
|
|
# ruby AppLovinQualityServiceSetup-xyz.rb (when no parameters given, invokes the "install" command)
|
||
|
|
# ruby AppLovinQualityServiceSetup-xyz.rb install -targetname MyTarget (installs on target name "MyTarget")
|
||
|
|
# ruby AppLovinQualityServiceSetup-xyz.rb uninstall
|
||
|
|
# ruby AppLovinQualityServiceSetup-xyz.rb uninstall -targetname MyTarget
|
||
|
|
# ----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||
|
|
require 'net/http'
|
||
|
|
require 'openssl'
|
||
|
|
require 'rexml/document'
|
||
|
|
require 'fileutils'
|
||
|
|
require 'date'
|
||
|
|
require 'digest'
|
||
|
|
require 'json'
|
||
|
|
require 'base64'
|
||
|
|
require 'tmpdir'
|
||
|
|
require 'securerandom'
|
||
|
|
|
||
|
|
|
||
|
|
# Developer-specific ID
|
||
|
|
application_data=<<APPLICATION_DATA
|
||
|
|
{
|
||
|
|
"api_key": "3FwgJWp0mc9soq0XbUerSfpAsO2UzxClyUTe9FtEngqvczhSNObAztMpDFMy7HXP7ynJubciGU6fpOCx5R96_t"
|
||
|
|
}
|
||
|
|
APPLICATION_DATA
|
||
|
|
|
||
|
|
|
||
|
|
SCRIPT_FETCH_URI = "https://api2.safedk.com/v1/build/ios_setup2"
|
||
|
|
|
||
|
|
MAVEN_REPO = "artifacts.applovin.com/ios"
|
||
|
|
MAVEN_USER = nil
|
||
|
|
MAVEN_PASSWORD = nil
|
||
|
|
|
||
|
|
|
||
|
|
@installer = "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
|
||
|
|
|
||
|
|
# =====================================================================================
|
||
|
|
|
||
|
|
@version = '7.12.6'
|
||
|
|
|
||
|
|
# Internal
|
||
|
|
class String
|
||
|
|
def escape
|
||
|
|
self.force_encoding("UTF-8").gsub(/[\\\"`$]/){|m| '\\' + m }
|
||
|
|
end
|
||
|
|
def description
|
||
|
|
self.sub(".pre.","-")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
MAVEN_GROUP = "com/applovin/quality"
|
||
|
|
MAVEN_GROUP_ID = "com.applovin.quality"
|
||
|
|
MAVEN_ARTIFACT_IDS = ["AppLovinQualityServicePod2"]
|
||
|
|
MAVEN_ARTIFACT_TYPE = "zip"
|
||
|
|
|
||
|
|
SERVICE_NAME = "AppLovinQualityService"
|
||
|
|
HOME_PATH = File.expand_path('~')
|
||
|
|
REPO_DOMAIN = MAVEN_REPO.split('/').first
|
||
|
|
POD_REPO_PATHS = ["#{HOME_PATH}/.#{SERVICE_NAME}/#{REPO_DOMAIN}/iOSPodsRepo2"]
|
||
|
|
SCRIPT_PATH = File.expand_path(__FILE__)
|
||
|
|
SCRIPT_DIR = File.dirname(SCRIPT_PATH)
|
||
|
|
APPLOVIN_QUALITY_SERVICE_DIR = SERVICE_NAME
|
||
|
|
APPLOVIN_QUALITY_SERVICE_PATH = "#{SCRIPT_DIR}/#{APPLOVIN_QUALITY_SERVICE_DIR}"
|
||
|
|
SCRIPT_NAME = File.basename($0).escape
|
||
|
|
BUILD_ROOT = File.dirname(ENV["BUILD_ROOT"]) rescue nil
|
||
|
|
BUILD_ROOT_TMP = BUILD_ROOT.nil? ? nil : BUILD_ROOT + ".tmp"
|
||
|
|
|
||
|
|
LAST_UPDATE_PATH = "#{HOME_PATH}/.#{SERVICE_NAME}/repos_last_update.txt"
|
||
|
|
LAST_USED_FILE = "last_used.txt"
|
||
|
|
VERSION_FILE = "version.txt"
|
||
|
|
INSTALLER_APP = "#{SERVICE_NAME}Installer.app"
|
||
|
|
INSTALLER_EXECUTABLE = "Contents/MacOS/#{SERVICE_NAME}Installer"
|
||
|
|
PLUGIN_APP = "#{SERVICE_NAME}Plugin.app"
|
||
|
|
SETUP_SCRIPT = "#{SERVICE_NAME}Setup.rb"
|
||
|
|
CLIENT_FRAMEWORK = "#{SERVICE_NAME}.framework"
|
||
|
|
CLIENT_XCFRAMEWORK = "#{SERVICE_NAME}.xcframework"
|
||
|
|
CLIENT_FRAMEWORK_DSYM = "#{CLIENT_FRAMEWORK}.dSYM"
|
||
|
|
LICENSE_FILE = "APPLOVIN-LICENSE.txt"
|
||
|
|
LICENSES_DIR = "Third-Party-Licenses"
|
||
|
|
CHECKSUM_PREFIX = "\n#MD5="
|
||
|
|
CHECK_INTERVAL = 24*3600
|
||
|
|
CLEANUP_INTERVAL = 90*24*3600
|
||
|
|
LOWEST_VERSION = Gem::Version.new('0')
|
||
|
|
CUTOFF_VERSION = Gem::Version.new('5.0.0')
|
||
|
|
|
||
|
|
@specific_version = nil
|
||
|
|
@target_id = nil
|
||
|
|
@target_name = nil
|
||
|
|
@local_frameworks = nil
|
||
|
|
@nofetch = false
|
||
|
|
@noupdater = false
|
||
|
|
|
||
|
|
CMD_INSTALL = "install"
|
||
|
|
CMD_UNINSTALL = "uninstall"
|
||
|
|
CMD_UPDATE = "update"
|
||
|
|
CMD_NOFETCH = "nofetch"
|
||
|
|
|
||
|
|
SUPPORT_EMAIL = "devsupport@applovin.com"
|
||
|
|
|
||
|
|
|
||
|
|
def get_own_api_key
|
||
|
|
|
||
|
|
app_setup_content = File.read(__FILE__)
|
||
|
|
|
||
|
|
app_data_match = app_setup_content.scan(/(APPLICATION\_DATA.*APPLICATION\_DATA)/m)
|
||
|
|
return nil if app_data_match.count == 0
|
||
|
|
|
||
|
|
app_data = app_data_match.last.first
|
||
|
|
content_json = app_data.gsub(/APPLICATION\_DATA/,'')
|
||
|
|
content = JSON.parse(content_json)
|
||
|
|
api_key = content["api_key"]
|
||
|
|
api_key = nil if api_key == ''
|
||
|
|
api_key
|
||
|
|
|
||
|
|
rescue Exception => msg
|
||
|
|
raise "Failed to find api_key in #{__FILE__} file, reason: #{msg}"
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def fetch_setup_script
|
||
|
|
|
||
|
|
read_last_update
|
||
|
|
return if ((Time.now - @last_update) < CHECK_INTERVAL) && @specific_version.nil?
|
||
|
|
return if !@specific_version.nil? && @specific_version == Gem::Version.new(@version)
|
||
|
|
|
||
|
|
api_key = get_own_api_key
|
||
|
|
uri = URI(SCRIPT_FETCH_URI)
|
||
|
|
puts @specific_version.nil? ? "Requesting latest setup script from #{uri.host}..." : "Requesting setup script version #{@specific_version.to_s.description} from #{uri.host}..."
|
||
|
|
|
||
|
|
postdata = {:api_key => api_key, :maven_repo => MAVEN_REPO}
|
||
|
|
postdata[:maven_user] = MAVEN_USER unless MAVEN_USER.nil?
|
||
|
|
postdata[:maven_password] = MAVEN_PASSWORD unless MAVEN_PASSWORD.nil?
|
||
|
|
postdata[:setup_script_version] = @version unless @version.nil?
|
||
|
|
postdata[:requested_script_version] = @specific_version.to_s.description unless @specific_version.nil?
|
||
|
|
res = Net::HTTP.post(uri, postdata.to_json, "Content-Type" => "application/json")
|
||
|
|
status_code = res.code.to_i
|
||
|
|
if status_code == 304
|
||
|
|
puts "You already have the latest setup script."
|
||
|
|
return
|
||
|
|
end
|
||
|
|
|
||
|
|
if status_code >= 300
|
||
|
|
msg = "Failed to fetch setup script, status code: #{status_code}"
|
||
|
|
if status_code >= 400 && status_code < 500 && !@specific_version.nil?
|
||
|
|
raise msg + ". Version #{@specific_version.to_s.description} may not exist"
|
||
|
|
else
|
||
|
|
raise msg
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
raise "Failed to fetch setup script" if res.body.nil?
|
||
|
|
md5_index = res.body.index(CHECKSUM_PREFIX)
|
||
|
|
raise "Missing file checksum" if md5_index.nil?
|
||
|
|
raise "Illegal setup file structure" if res.body.length < 7
|
||
|
|
|
||
|
|
file_checksum = res.body[(md5_index+CHECKSUM_PREFIX.length)..(res.body.length-1)]
|
||
|
|
original_setup_file = res.body[0..md5_index - 1]
|
||
|
|
content_md5 = Digest::MD5.hexdigest(original_setup_file)
|
||
|
|
raise "Setup file checksum failed" if content_md5 != file_checksum
|
||
|
|
|
||
|
|
Dir.mktmpdir do |tmpdir|
|
||
|
|
tmp_script_path = "#{tmpdir}/#{SCRIPT_NAME}"
|
||
|
|
File.open(tmp_script_path, 'w') { |file| file.write(res.body) }
|
||
|
|
FileUtils.mv(tmp_script_path, SCRIPT_PATH) if File.exist?(tmp_script_path)
|
||
|
|
puts "Setup script has been updated."
|
||
|
|
end
|
||
|
|
|
||
|
|
rescue Exception => e
|
||
|
|
raise e.message if e.message.include?("code: 4")
|
||
|
|
puts "Error downloading setup script: #{e.message}.\nSetup script will not be updated in this run."
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def read_last_update
|
||
|
|
@last_update = Time.at(0)
|
||
|
|
content = File.read(LAST_UPDATE_PATH)
|
||
|
|
return unless content
|
||
|
|
@last_update = DateTime.iso8601(content.strip).to_time
|
||
|
|
rescue Exception
|
||
|
|
# ignore
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def update_last_update
|
||
|
|
File.open(LAST_UPDATE_PATH, 'w') { |file| file.write(DateTime.now.to_s) } rescue nil
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def read_pod_version
|
||
|
|
path = "#{APPLOVIN_QUALITY_SERVICE_PATH}/#{VERSION_FILE}"
|
||
|
|
content = File.read(path)
|
||
|
|
Gem::Version.new(content)
|
||
|
|
rescue Exception
|
||
|
|
LOWEST_VERSION
|
||
|
|
end
|
||
|
|
|
||
|
|
def clean_repo_periodically(repo_path)
|
||
|
|
return unless File.exist?(repo_path)
|
||
|
|
dirs = Dir.entries(repo_path).select{|f| f != '.' && f != '..' && File.directory?(File.join(repo_path, f)) }
|
||
|
|
dirs.each do |dir|
|
||
|
|
path = "#{repo_path}/#{dir}"
|
||
|
|
last_used_path = "#{path}/#{LAST_USED_FILE}"
|
||
|
|
content = File.read(last_used_path) rescue nil
|
||
|
|
next unless content
|
||
|
|
last_used = DateTime.iso8601(content.strip).to_time rescue Time.at(0)
|
||
|
|
FileUtils.rm_rf(path) if (Time.now - last_used) > CLEANUP_INTERVAL
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def init_repo(repo_path)
|
||
|
|
repo = {path: repo_path}
|
||
|
|
FileUtils.mkdir_p repo_path
|
||
|
|
|
||
|
|
dirs = Dir.entries(repo_path).select{|f| f != '.' && f != '..' && File.directory?(File.join(repo_path, f)) }
|
||
|
|
repo[:local_versions] = dirs.map{|dir| Gem::Version.new(dir) rescue nil}.compact.sort
|
||
|
|
repo[:highest_local_version] = repo[:local_versions].max || LOWEST_VERSION
|
||
|
|
repo
|
||
|
|
end
|
||
|
|
|
||
|
|
def init_and_cleanup
|
||
|
|
@repos = POD_REPO_PATHS.map{|repo_path| init_repo(repo_path)}
|
||
|
|
|
||
|
|
# cleaning old versions from repos
|
||
|
|
POD_REPO_PATHS.map{|repo_path| clean_repo_periodically(repo_path)}
|
||
|
|
|
||
|
|
MAVEN_ARTIFACT_IDS.each_with_index {|artifact_id, i| @repos[i][:artifact_id] = artifact_id}
|
||
|
|
@repos[0][:minimal_version] = CUTOFF_VERSION
|
||
|
|
|
||
|
|
read_last_update
|
||
|
|
|
||
|
|
FileUtils.mkdir_p APPLOVIN_QUALITY_SERVICE_PATH
|
||
|
|
@previous_pod_version = read_pod_version
|
||
|
|
FileUtils.rm_rf BUILD_ROOT_TMP rescue nil
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def confirm_xcode_projects_exist
|
||
|
|
project_paths = Dir.glob("#{SCRIPT_DIR}/*.xcodeproj")
|
||
|
|
raise "Could not find Xcode project file(s) under #{SCRIPT_DIR}\nPlease copy this script to your Xcode project directory and run it from there." unless project_paths.count > 0
|
||
|
|
project_paths
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def update_last_used(pod_zip)
|
||
|
|
last_used_path = "#{File.dirname(pod_zip)}/#{LAST_USED_FILE}"
|
||
|
|
File.open(last_used_path, 'w') { |file| file.write(DateTime.now.to_s) } rescue nil
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def fetch(uri_str, limit=10)
|
||
|
|
raise 'Too many HTTP redirects' if limit == 0
|
||
|
|
uri = URI(uri_str)
|
||
|
|
Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https', :verify_mode => OpenSSL::SSL::VERIFY_NONE, :open_timeout => 120) do |http|
|
||
|
|
request = Net::HTTP::Get.new uri.request_uri
|
||
|
|
request.basic_auth MAVEN_USER, MAVEN_PASSWORD unless MAVEN_USER.nil?
|
||
|
|
resp = http.request(request)
|
||
|
|
case resp
|
||
|
|
when Net::HTTPSuccess
|
||
|
|
resp.body
|
||
|
|
when Net::HTTPRedirection
|
||
|
|
location = resp['location']
|
||
|
|
fetch(location, limit - 1)
|
||
|
|
else
|
||
|
|
raise "Server response code: #{resp.code}"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
rescue Exception => e
|
||
|
|
raise "AppLovin Maven repository returned the following error: #{e.message}"
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def get_latest_remote_version(artifact_id)
|
||
|
|
begin
|
||
|
|
uri = URI("https://#{MAVEN_REPO}/#{MAVEN_GROUP}/#{artifact_id}/maven-metadata.xml")
|
||
|
|
maven_data = fetch(uri)
|
||
|
|
rescue Exception => e
|
||
|
|
raise "Failed to access AppLovin Maven repository: #{e.message}"
|
||
|
|
end
|
||
|
|
|
||
|
|
begin
|
||
|
|
doc = REXML::Document.new maven_data
|
||
|
|
group_id = doc.root.elements["groupId"].text
|
||
|
|
found_artifact_id = doc.root.elements["artifactId"].text
|
||
|
|
version = doc.root.elements["versioning/release"].text
|
||
|
|
rescue Exception => e
|
||
|
|
raise "Failed to parse maven metadata received from server: #{e.message}"
|
||
|
|
end
|
||
|
|
|
||
|
|
# Some basic validation
|
||
|
|
if group_id != MAVEN_GROUP_ID || found_artifact_id != artifact_id
|
||
|
|
raise "Failed to properly parse Maven metadata"
|
||
|
|
end
|
||
|
|
|
||
|
|
Gem::Version.new(version) rescue LOWEST_VERSION
|
||
|
|
|
||
|
|
rescue Exception => e
|
||
|
|
puts e.message
|
||
|
|
nil
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def download_pod(repo, version)
|
||
|
|
repo_path = repo[:path]
|
||
|
|
artifact_id = repo[:artifact_id]
|
||
|
|
version_str = version.to_s.description
|
||
|
|
|
||
|
|
pod_zip = "#{repo_path}/#{version_str}/#{artifact_id}-#{version_str}.zip"
|
||
|
|
puts "Downloading #{SERVICE_NAME} Pod version #{version_str}..."
|
||
|
|
|
||
|
|
uri = URI("https://#{MAVEN_REPO}/#{MAVEN_GROUP}/#{artifact_id}/#{version_str}/#{artifact_id}-#{version_str}.#{MAVEN_ARTIFACT_TYPE}")
|
||
|
|
content = fetch(uri)
|
||
|
|
|
||
|
|
puts "Validating #{SERVICE_NAME} Pod checksum..."
|
||
|
|
uri = URI("https://#{MAVEN_REPO}/#{MAVEN_GROUP}/#{artifact_id}/#{version_str}/#{artifact_id}-#{version_str}.#{MAVEN_ARTIFACT_TYPE}.md5")
|
||
|
|
md5 = fetch(uri)
|
||
|
|
raise "Failed to validate Pod MD5 checksum" unless Digest::MD5.hexdigest(content) == md5
|
||
|
|
|
||
|
|
FileUtils.mkdir_p "#{repo_path}/#{version_str}"
|
||
|
|
open(pod_zip, "wb") do |file|
|
||
|
|
file.write(content)
|
||
|
|
end
|
||
|
|
|
||
|
|
pod_zip
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def get_pod_zip
|
||
|
|
|
||
|
|
if @specific_version
|
||
|
|
@repos.each do |repo|
|
||
|
|
if repo[:local_versions].include?(@specific_version)
|
||
|
|
version_str = @specific_version.to_s.description
|
||
|
|
pod_zip = "#{repo[:path]}/#{version_str}/#{repo[:artifact_id]}-#{version_str}.zip"
|
||
|
|
if File.exist?(pod_zip)
|
||
|
|
puts "Requested version #{version_str} of #{SERVICE_NAME} Pod found locally"
|
||
|
|
return pod_zip
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
repos = @repos
|
||
|
|
update_required = false
|
||
|
|
version = @specific_version
|
||
|
|
if version.nil?
|
||
|
|
repos.each do |repo|
|
||
|
|
if repo[:highest_local_version] && @command != CMD_INSTALL
|
||
|
|
if (Time.now - @last_update) < CHECK_INTERVAL
|
||
|
|
version = [version || LOWEST_VERSION, repo[:highest_local_version]].max
|
||
|
|
else
|
||
|
|
update_required = true
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
puts "#{SERVICE_NAME} Pod was updated within the last #{CHECK_INTERVAL.to_i / 3600} hours, no update required" unless update_required || version.nil? || version != Gem::Version.new(@version)
|
||
|
|
|
||
|
|
if version.nil?
|
||
|
|
remote_version = repos.map{|repo| get_latest_remote_version(repo[:artifact_id])}.compact.max
|
||
|
|
highest_local_version = repos.map{|repo| repo[:highest_local_version]}.max
|
||
|
|
if remote_version
|
||
|
|
puts "#{SERVICE_NAME} Pod has the latest version, no update required" if highest_local_version == remote_version
|
||
|
|
update_last_update
|
||
|
|
version = remote_version
|
||
|
|
else
|
||
|
|
version = highest_local_version
|
||
|
|
end
|
||
|
|
end
|
||
|
|
raise "#{SERVICE_NAME} Pod version could not be evaluated" if version.nil? || version == LOWEST_VERSION
|
||
|
|
|
||
|
|
repo = @repos.select{|repo| version >= repo[:minimal_version]}.last
|
||
|
|
raise "Requested version too low: #{version}, aborting download" unless repo.size > 0
|
||
|
|
version_str = version.to_s.description
|
||
|
|
pod_zip = "#{repo[:path]}/#{version_str}/#{repo[:artifact_id]}-#{version_str}.zip"
|
||
|
|
pod_zip = download_pod(repo, version) unless File.exist?(pod_zip)
|
||
|
|
pod_zip
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def dirs_identical?(*dirs)
|
||
|
|
signatures = dirs.map{|dir| Dir["#{dir}/**/*"].select{|p| File.file?(p)}.sort.map{|p| Digest::MD5.hexdigest(File.read(p)) rescue nil}.compact.join}
|
||
|
|
signatures.uniq.size == 1
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def copy_dsym_to_products_dir
|
||
|
|
built_products_dir = ENV["BUILT_PRODUCTS_DIR"]
|
||
|
|
return if built_products_dir.nil?
|
||
|
|
src = "#{APPLOVIN_QUALITY_SERVICE_PATH}/#{CLIENT_FRAMEWORK_DSYM}"
|
||
|
|
return unless File.exist?(src)
|
||
|
|
dest = "#{built_products_dir}/#{CLIENT_FRAMEWORK_DSYM}"
|
||
|
|
return if File.exist?(dest) && dirs_identical?(src, dest)
|
||
|
|
FileUtils.rm_rf(dest)
|
||
|
|
FileUtils.cp_r(src, dest)
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def refresh_framework_in_products_dir
|
||
|
|
built_products_dir = ENV["BUILT_PRODUCTS_DIR"]
|
||
|
|
return if built_products_dir.nil?
|
||
|
|
sdk_name = ENV["SDK_NAME"]
|
||
|
|
return if sdk_name.nil?
|
||
|
|
dest = "#{built_products_dir}/#{CLIENT_FRAMEWORK}"
|
||
|
|
return unless File.exist?(dest)
|
||
|
|
return unless File.exist?("#{APPLOVIN_QUALITY_SERVICE_PATH}/#{CLIENT_XCFRAMEWORK}")
|
||
|
|
|
||
|
|
framework_paths = Dir["#{APPLOVIN_QUALITY_SERVICE_PATH}/#{CLIENT_XCFRAMEWORK}/ios-*"]
|
||
|
|
simulator_framework_path = framework_paths.select{|p|p.include?("x86")}.first
|
||
|
|
device_framework_path = framework_paths.select{|p| !p.include?("x86")}.first
|
||
|
|
return if simulator_framework_path.nil? || device_framework_path.nil?
|
||
|
|
|
||
|
|
framework_path = sdk_name.start_with?("iphonesimulator") ? simulator_framework_path : device_framework_path
|
||
|
|
src = "#{framework_path}/#{CLIENT_FRAMEWORK}"
|
||
|
|
return if dirs_identical?(src, dest)
|
||
|
|
|
||
|
|
FileUtils.rm_rf(dest)
|
||
|
|
FileUtils.cp_r(src, built_products_dir)
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def inflate_pod(pod_zip)
|
||
|
|
Dir.mktmpdir do |tmpdir|
|
||
|
|
puts "Extracting Pod #{File.basename(pod_zip)}..."
|
||
|
|
dest = File.join(tmpdir,SERVICE_NAME)
|
||
|
|
is_ok = system("unzip -qq -o \"#{pod_zip}\" -d \"#{dest}\"")
|
||
|
|
raise "Failed to unzip the #{SERVICE_NAME} Pod" unless is_ok
|
||
|
|
|
||
|
|
unless dirs_identical?(APPLOVIN_QUALITY_SERVICE_PATH, dest)
|
||
|
|
FileUtils.rm_rf(APPLOVIN_QUALITY_SERVICE_PATH)
|
||
|
|
FileUtils.mv(dest, APPLOVIN_QUALITY_SERVICE_PATH)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
update_last_used(pod_zip)
|
||
|
|
copy_dsym_to_products_dir
|
||
|
|
refresh_framework_in_products_dir
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def inflate_installer(tmpdir)
|
||
|
|
installer_path = "#{tmpdir}/AppLovinQualityServiceInstaller.app"
|
||
|
|
FileUtils.mkdir_p installer_path
|
||
|
|
installer_zip_path = "#{installer_path}.zip"
|
||
|
|
installer_zip_content = Base64.strict_decode64(@installer)
|
||
|
|
File.open(installer_zip_path, 'w') { |file| file.write(installer_zip_content) }
|
||
|
|
is_ok = system("unzip -qq -o \"#{installer_zip_path}\" -d \"#{installer_path}\"")
|
||
|
|
raise "Failed to inflate installer into #{tmpdir}" unless is_ok
|
||
|
|
installer_path
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def exec_valid?(installer_exec)
|
||
|
|
return false unless File.exist?(installer_exec)
|
||
|
|
return false unless File.size(installer_exec) > 1000
|
||
|
|
true
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def invoke_installer
|
||
|
|
Dir.mktmpdir do |tmpdir|
|
||
|
|
installer_path = inflate_installer(tmpdir)
|
||
|
|
installer_exec = "#{installer_path}/#{INSTALLER_EXECUTABLE}"
|
||
|
|
additional_params = ""
|
||
|
|
additional_params += " -targetid \"#{@target_id}\"" if @target_id
|
||
|
|
additional_params += " -targetname \"#{@target_name}\"" if @target_name
|
||
|
|
additional_params += " -localframeworks \"#{@local_frameworks}\"" if @local_frameworks
|
||
|
|
additional_params += " -noupdater" if @noupdater
|
||
|
|
invocation = "\"#{installer_exec}\" #{@command} -projectdir \"#{SCRIPT_DIR}\" -setupfile \"#{SCRIPT_NAME}\" -nosig #{additional_params}"
|
||
|
|
invocation += " -version \"#{@specific_version.to_s.description}\"" if @specific_version
|
||
|
|
is_ok = system(invocation)
|
||
|
|
unless is_ok
|
||
|
|
raise "Installer executable cannot be found. It may happen due to antivirus software blocking it" unless exec_valid?(installer_exec)
|
||
|
|
raise "Failed to apply the #{SERVICE_NAME} Pod"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def move_build_root
|
||
|
|
FileUtils.mv(BUILD_ROOT, BUILD_ROOT_TMP) rescue nil unless BUILD_ROOT.nil?
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def update_setup
|
||
|
|
pod_version = read_pod_version
|
||
|
|
if @specific_version && (@specific_version < CUTOFF_VERSION)
|
||
|
|
pod_setup_path = "#{APPLOVIN_QUALITY_SERVICE_PATH}/#{SETUP_SCRIPT}"
|
||
|
|
pod_setup_content = File.read(pod_setup_path) rescue nil
|
||
|
|
return if pod_setup_content.nil?
|
||
|
|
|
||
|
|
current_setup_content = File.read(SCRIPT_PATH)
|
||
|
|
|
||
|
|
["MAVEN_REPO","MAVEN_USER","MAVEN_PASSWORD"].each do |symbol|
|
||
|
|
original_line = current_setup_content[/#{symbol}\s*=.*?$/]
|
||
|
|
pod_setup_content.gsub!(/#{symbol}\s*=.*?$/, original_line)
|
||
|
|
end
|
||
|
|
|
||
|
|
app_data_match = current_setup_content.scan(/(APPLICATION\_DATA.*APPLICATION\_DATA)/m)
|
||
|
|
return if app_data_match.count == 0
|
||
|
|
|
||
|
|
app_data = app_data_match.last.first
|
||
|
|
pod_setup_content.gsub!(/APPLICATION\_DATA.*APPLICATION\_DATA/m, app_data)
|
||
|
|
File.write(SCRIPT_PATH, pod_setup_content)
|
||
|
|
puts "Updated setup script to version #{pod_version}"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def read_arg(i)
|
||
|
|
if i+1 < ARGV.count
|
||
|
|
return ARGV[i+1], i+1
|
||
|
|
else
|
||
|
|
return nil, i
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def read_specific_version(version_str)
|
||
|
|
begin
|
||
|
|
@specific_version = Gem::Version.new(version_str)
|
||
|
|
rescue
|
||
|
|
raise("Illegal version string: #{version_str}")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def read_command_line_options(i)
|
||
|
|
while i < ARGV.count
|
||
|
|
case ARGV[i]
|
||
|
|
when "-targetid"
|
||
|
|
@target_id, i = read_arg(i)
|
||
|
|
|
||
|
|
when "-targetname"
|
||
|
|
@target_name, i = read_arg(i)
|
||
|
|
|
||
|
|
when "-localframeworks"
|
||
|
|
@local_frameworks, i = read_arg(i)
|
||
|
|
|
||
|
|
when "-version"
|
||
|
|
specific_version, i = read_arg(i)
|
||
|
|
read_specific_version(specific_version)
|
||
|
|
|
||
|
|
when "-noupdater"
|
||
|
|
@noupdater = true
|
||
|
|
|
||
|
|
else
|
||
|
|
raise "Unrecognized command line option #{ARGV[i]}"
|
||
|
|
end
|
||
|
|
i += 1
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def read_command_line_args
|
||
|
|
@nofetch = ARGV[0] == CMD_NOFETCH
|
||
|
|
i = @nofetch ? 1 : 0
|
||
|
|
@command = case ARGV[i]
|
||
|
|
when nil
|
||
|
|
CMD_INSTALL
|
||
|
|
|
||
|
|
when CMD_INSTALL
|
||
|
|
read_command_line_options(i+1)
|
||
|
|
CMD_INSTALL
|
||
|
|
|
||
|
|
when CMD_UNINSTALL
|
||
|
|
read_command_line_options(i+1)
|
||
|
|
CMD_UNINSTALL
|
||
|
|
|
||
|
|
when CMD_UPDATE
|
||
|
|
if ARGV[i+1]
|
||
|
|
if ARGV[i+1].start_with?('-')
|
||
|
|
read_command_line_options(i+1)
|
||
|
|
else
|
||
|
|
read_specific_version(ARGV[i+1])
|
||
|
|
end
|
||
|
|
end
|
||
|
|
CMD_UPDATE
|
||
|
|
|
||
|
|
else
|
||
|
|
raise "Unrecognized command: #{ARGV[i]}\n" +
|
||
|
|
"Usage: ruby #{File.basename($0)} <command> <options>\n" +
|
||
|
|
"Where:\n" +
|
||
|
|
" command - #{CMD_INSTALL} | #{CMD_UNINSTALL} | #{CMD_UPDATE}\n"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def print_end_message
|
||
|
|
if @command == CMD_INSTALL
|
||
|
|
puts "If you decide to uninstall the #{SERVICE_NAME} Pod at any stage, you may run:"
|
||
|
|
puts "ruby #{File.basename($0)} uninstall"
|
||
|
|
puts "In case you encounter any issues, please contact us at #{SUPPORT_EMAIL}"
|
||
|
|
elsif @command == CMD_UNINSTALL
|
||
|
|
puts "Please make sure run 'clean' before rebuilding your project."
|
||
|
|
puts "If you uninstalled #{SERVICE_NAME} from all of your targets and projects and no longer need it,"
|
||
|
|
puts "you may manually delete the #{APPLOVIN_QUALITY_SERVICE_DIR} directory under your project,"
|
||
|
|
puts "as well as delete this #{File.basename($0)} script."
|
||
|
|
puts "In case you encounter any issues, please contact us at #{SUPPORT_EMAIL}"
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
def print_framework_update_message(operation, framework_name)
|
||
|
|
puts "--------------------NOTE--------------------"
|
||
|
|
puts "Project file has been #{operation} to include #{framework_name}."
|
||
|
|
puts "This operation is done once and causes the build to stop."
|
||
|
|
puts "All should work fine once you clean your project and re-build it."
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
# === Start of script ====
|
||
|
|
|
||
|
|
begin
|
||
|
|
STDOUT.sync = true
|
||
|
|
read_command_line_args
|
||
|
|
if @nofetch
|
||
|
|
puts "Executing script version: #{@version}" if @version
|
||
|
|
init_and_cleanup
|
||
|
|
if @command == CMD_UPDATE
|
||
|
|
pod_zip = get_pod_zip
|
||
|
|
inflate_pod(pod_zip)
|
||
|
|
else
|
||
|
|
confirm_xcode_projects_exist
|
||
|
|
unless @command == CMD_UNINSTALL
|
||
|
|
pod_zip = get_pod_zip
|
||
|
|
inflate_pod(pod_zip)
|
||
|
|
end
|
||
|
|
puts
|
||
|
|
invoke_installer
|
||
|
|
print_end_message
|
||
|
|
end
|
||
|
|
update_setup
|
||
|
|
puts "..DONE"
|
||
|
|
else
|
||
|
|
puts "AppLovin Quality Service Xcode Setup Script"
|
||
|
|
puts "Copyright (c) 2026 AppLovin. All Rights Reserved."
|
||
|
|
puts "Script version: #{@version}" if @version
|
||
|
|
fetch_setup_script
|
||
|
|
args = ARGV.drop(1).map{|arg| '"' + arg + '"'}.join(' ')
|
||
|
|
exec("ruby \"#{SCRIPT_PATH}\" #{CMD_NOFETCH} #{@command} #{args}")
|
||
|
|
end
|
||
|
|
|
||
|
|
rescue Exception => e
|
||
|
|
abort("\n#{e.message}\n#{SERVICE_NAME} setup FAILED\n\n")
|
||
|
|
end
|
||
|
|
|
||
|
|
#MD5=dd470e6093b3c9035e92be102ff49c39
|