blob: cf07812afd3c7139f086b108aed779e39af251cf [file] [log] [blame]
require 'active_support/core_ext/string/inflections'
module Pod
class Installer
class UserProjectIntegrator
# This class is responsible for integrating the library generated by a
# {TargetDefinition} with its destination project.
#
class TargetIntegrator
autoload :XCConfigIntegrator, 'cocoapods/installer/user_project_integrator/target_integrator/xcconfig_integrator'
# @return [String] the string to use as prefix for every build phase added to the user project
#
BUILD_PHASE_PREFIX = '[CP] '.freeze
# @return [String] the string to use as prefix for every build phase declared by the user within a podfile
# or podspec.
#
USER_BUILD_PHASE_PREFIX = '[CP-User] '.freeze
# @return [String] the name of the check manifest phase
#
CHECK_MANIFEST_PHASE_NAME = 'Check Pods Manifest.lock'.freeze
# @return [Array<Symbol>] the symbol types, which require that the pod
# frameworks are embedded in the output directory / product bundle.
#
# @note This does not include :app_extension or :watch_extension because
# these types must have their frameworks embedded in their host targets.
# For messages extensions, this only applies if it's embedded in a messages
# application.
#
EMBED_FRAMEWORK_TARGET_TYPES = [:application, :unit_test_bundle, :ui_test_bundle, :watch2_extension, :messages_application].freeze
# @return [String] the name of the embed frameworks phase
#
EMBED_FRAMEWORK_PHASE_NAME = 'Embed Pods Frameworks'.freeze
# @return [String] the name of the copy resources phase
#
COPY_PODS_RESOURCES_PHASE_NAME = 'Copy Pods Resources'.freeze
# @return [AggregateTarget] the target that should be integrated.
#
attr_reader :target
# Init a new TargetIntegrator
#
# @param [AggregateTarget] target @see #target
#
def initialize(target)
@target = target
end
class << self
# Adds a shell script build phase responsible to copy (embed) the frameworks
# generated by the TargetDefinition to the bundle of the product of the
# targets.
#
# @param [PBXNativeTarget] native_target
# The native target to add the script phase into.
#
# @param [String] script_path
# The script path to execute as part of this script phase.
#
# @param [Array<String>] input_paths
# The input paths (if any) to include for this script phase.
#
# @param [Array<String>] output_paths
# The output paths (if any) to include for this script phase.
#
# @return [void]
#
def add_embed_frameworks_script_phase_to_target(native_target, script_path, input_paths = [], output_paths = [])
phase = TargetIntegrator.create_or_update_build_phase(native_target, BUILD_PHASE_PREFIX + EMBED_FRAMEWORK_PHASE_NAME)
phase.shell_script = %("#{script_path}"\n)
unless input_paths.empty?
phase.input_paths = input_paths
end
unless output_paths.empty?
phase.output_paths = output_paths
end
end
# Delete a 'Embed Pods Frameworks' Copy Files Build Phase if present
#
# @param [PBXNativeTarget] native_target
# The native target to remove the script phase from.
#
def remove_embed_frameworks_script_phase_from_target(native_target)
embed_build_phase = native_target.shell_script_build_phases.find { |bp| bp.name && bp.name.end_with?(EMBED_FRAMEWORK_PHASE_NAME) }
return unless embed_build_phase.present?
native_target.build_phases.delete(embed_build_phase)
end
# Adds a shell script build phase responsible to copy the resources
# generated by the TargetDefinition to the bundle of the product of the
# targets.
#
# @param [PBXNativeTarget] native_target
# The native target to add the script phase into.
#
# @param [String] script_path
# The script path to execute as part of this script phase.
#
# @param [Array<String>] input_paths
# The input paths (if any) to include for this script phase.
#
# @param [Array<String>] output_paths
# The output paths (if any) to include for this script phase.
#
# @return [void]
#
def add_copy_resources_script_phase_to_target(native_target, script_path, input_paths = [], output_paths = [])
phase_name = COPY_PODS_RESOURCES_PHASE_NAME
phase = TargetIntegrator.create_or_update_build_phase(native_target, BUILD_PHASE_PREFIX + phase_name)
phase.shell_script = %("#{script_path}"\n)
unless input_paths.empty?
phase.input_paths = input_paths
end
unless output_paths.empty?
phase.output_paths = output_paths
end
end
# Creates or update a shell script build phase for the given target.
#
# @param [PBXNativeTarget] native_target
# The native target to add the script phase into.
#
# @param [String] phase_name
# The name of the phase to use.
#
# @param [Class] phase_class
# The class of the phase to use.
#
# @return [void]
#
def create_or_update_build_phase(native_target, phase_name, phase_class = Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
build_phases = native_target.build_phases.grep(phase_class)
build_phases.find { |phase| phase.name && phase.name.end_with?(phase_name) }.tap { |p| p.name = phase_name if p } ||
native_target.project.new(phase_class).tap do |phase|
UI.message("Adding Build Phase '#{phase_name}' to project.") do
phase.name = phase_name
phase.show_env_vars_in_log = '0'
native_target.build_phases << phase
end
end
end
# Updates all target script phases for the current target, including creating or updating, deleting
# and re-ordering.
#
# @return [void]
#
def create_or_update_user_script_phases(script_phases, native_target)
script_phase_names = script_phases.map { |k| k[:name] }
# Delete script phases no longer present in the target definition.
native_target_script_phases = native_target.shell_script_build_phases.select { |bp| !bp.name.nil? && bp.name.start_with?(USER_BUILD_PHASE_PREFIX) }
native_target_script_phases.each do |script_phase|
script_phase_name_without_prefix = script_phase.name.sub(USER_BUILD_PHASE_PREFIX, '')
unless script_phase_names.include?(script_phase_name_without_prefix)
native_target.build_phases.delete(script_phase)
end
end
# Create or update the ones that are expected to be.
script_phases.each do |td_script_phase|
name_with_prefix = USER_BUILD_PHASE_PREFIX + td_script_phase[:name]
phase = TargetIntegrator.create_or_update_build_phase(native_target, name_with_prefix)
phase.shell_script = td_script_phase[:script]
phase.shell_path = td_script_phase[:shell_path] if td_script_phase.key?(:shell_path)
phase.input_paths = td_script_phase[:input_files] if td_script_phase.key?(:input_files)
phase.output_paths = td_script_phase[:output_files] if td_script_phase.key?(:output_files)
phase.show_env_vars_in_log = td_script_phase[:show_env_vars_in_log] ? '1' : '0' if td_script_phase.key?(:show_env_vars_in_log)
execution_position = td_script_phase[:execution_position]
unless execution_position == :any
compile_build_phase_index = native_target.build_phases.index do |bp|
bp.is_a?(Xcodeproj::Project::Object::PBXSourcesBuildPhase)
end
unless compile_build_phase_index.nil?
script_phase_index = native_target.build_phases.index do |bp|
bp.is_a?(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) && !bp.name.nil? && bp.name == name_with_prefix
end
if (execution_position == :before_compile && script_phase_index > compile_build_phase_index) ||
(execution_position == :after_compile && script_phase_index < compile_build_phase_index)
native_target.build_phases.move_from(script_phase_index, compile_build_phase_index)
end
end
end
end
end
# Returns an extension in the target that corresponds to the
# resource's input extension.
#
# @return [String] The output extension.
#
def output_extension_for_resource(input_extension)
case input_extension
when '.storyboard' then '.storyboardc'
when '.xib' then '.nib'
when '.framework' then '.framework'
when '.xcdatamodel' then '.mom'
when '.xcdatamodeld' then '.momd'
when '.xcmappingmodel' then '.cdm'
else input_extension
end
end
end
# Integrates the user project targets. Only the targets that do **not**
# already have the Pods library in their frameworks build phase are
# processed.
#
# @return [void]
#
def integrate!
UI.section(integration_message) do
XCConfigIntegrator.integrate(target, native_targets)
add_pods_library
add_embed_frameworks_script_phase
remove_embed_frameworks_script_phase_from_embedded_targets
add_copy_resources_script_phase
add_check_manifest_lock_script_phase
add_user_script_phases
end
end
# @return [String] a string representation suitable for debugging.
#
def inspect
"#<#{self.class} for target `#{target.label}'>"
end
private
# @!group Integration steps
#---------------------------------------------------------------------#
# Adds spec product reference to the frameworks build phase of the
# {TargetDefinition} integration libraries. Adds a file reference to
# the frameworks group of the project and adds it to the frameworks
# build phase of the targets.
#
# @return [void]
#
def add_pods_library
frameworks = user_project.frameworks_group
native_targets.each do |native_target|
build_phase = native_target.frameworks_build_phase
# Find and delete possible reference for the other product type
old_product_name = target.requires_frameworks? ? target.static_library_name : target.framework_name
old_product_ref = frameworks.files.find { |f| f.path == old_product_name }
if old_product_ref.present?
UI.message("Removing old Pod product reference #{old_product_name} from project.")
build_phase.remove_file_reference(old_product_ref)
frameworks.remove_reference(old_product_ref)
end
# Find or create and add a reference for the current product type
target_basename = target.product_basename
new_product_ref = frameworks.files.find { |f| f.path == target.product_name } ||
frameworks.new_product_ref_for_target(target_basename, target.product_type)
build_phase.build_file(new_product_ref) ||
build_phase.add_file_reference(new_product_ref, true)
end
end
# Find or create a 'Copy Pods Resources' build phase
#
# @return [void]
#
def add_copy_resources_script_phase
native_targets.each do |native_target|
script_path = target.copy_resources_script_relative_path
resource_paths_by_config = target.resource_paths_by_config
input_paths = []
output_paths = []
unless resource_paths_by_config.values.all?(&:empty?)
resource_paths_flattened = resource_paths_by_config.values.flatten.uniq
input_paths = [target.copy_resources_script_relative_path, *resource_paths_flattened]
# convert input paths to output paths according to extensions
output_paths = resource_paths_flattened.map do |input_path|
base_path = '${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}'
output_extension = TargetIntegrator.output_extension_for_resource(File.extname(input_path))
File.join(base_path, File.basename(input_path, File.extname(input_path)) + output_extension)
end
end
TargetIntegrator.add_copy_resources_script_phase_to_target(native_target, script_path, input_paths, output_paths)
end
end
# Removes the embed frameworks build phase from embedded targets
#
# @note Older versions of CocoaPods would add this build phase to embedded
# targets. They should be removed on upgrade because embedded targets
# will have their frameworks embedded in their host targets.
#
def remove_embed_frameworks_script_phase_from_embedded_targets
return unless target.requires_host_target?
native_targets.each do |native_target|
if AggregateTarget::EMBED_FRAMEWORKS_IN_HOST_TARGET_TYPES.include? native_target.symbol_type
TargetIntegrator.remove_embed_frameworks_script_phase_from_target(native_target)
end
end
end
# Find or create a 'Embed Pods Frameworks' Copy Files Build Phase
#
# @return [void]
#
def add_embed_frameworks_script_phase
native_targets_to_embed_in.each do |native_target|
script_path = target.embed_frameworks_script_relative_path
framework_paths_by_config = target.framework_paths_by_config.values.flatten.uniq
input_paths = []
output_paths = []
unless framework_paths_by_config.all?(&:empty?)
input_paths = [target.embed_frameworks_script_relative_path, *framework_paths_by_config.map { |fw| [fw[:input_path], fw[:dsym_input_path]] }.flatten.compact]
output_paths = framework_paths_by_config.map { |fw| [fw[:output_path], fw[:dsym_output_path]] }.flatten.compact
end
TargetIntegrator.add_embed_frameworks_script_phase_to_target(native_target, script_path, input_paths, output_paths)
end
end
# Updates all target script phases for the current target, including creating or updating, deleting
# and re-ordering.
#
# @return [void]
#
def add_user_script_phases
native_targets.each do |native_target|
TargetIntegrator.create_or_update_user_script_phases(target.target_definition.script_phases, native_target)
end
end
# Adds a shell script build phase responsible for checking if the Pods
# locked in the Pods/Manifest.lock file are in sync with the Pods defined
# in the Podfile.lock.
#
# @note The build phase is appended to the front because to fail
# fast.
#
# @return [void]
#
def add_check_manifest_lock_script_phase
phase_name = CHECK_MANIFEST_PHASE_NAME
native_targets.each do |native_target|
phase = TargetIntegrator.create_or_update_build_phase(native_target, BUILD_PHASE_PREFIX + phase_name)
native_target.build_phases.unshift(phase).uniq! unless native_target.build_phases.first == phase
phase.shell_script = <<-SH.strip_heredoc
diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null
if [ $? != 0 ] ; then
# print error to STDERR
echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2
exit 1
fi
# This output is used by Xcode 'outputs' to avoid re-running this script phase.
echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"
SH
phase.input_paths = %w(${PODS_PODFILE_DIR_PATH}/Podfile.lock ${PODS_ROOT}/Manifest.lock)
phase.output_paths = [target.check_manifest_lock_script_output_file_path]
end
end
private
# @!group Private Helpers
#---------------------------------------------------------------------#
# @return [Array<PBXNativeTarget>] The list of all the targets that
# match the given target.
#
def native_targets
@native_targets ||= target.user_targets
end
# @return [Array<PBXNativeTarget>] The list of all the targets that
# require that the pod frameworks are embedded in the output
# directory / product bundle.
#
def native_targets_to_embed_in
return [] if target.requires_host_target?
native_targets.select do |target|
EMBED_FRAMEWORK_TARGET_TYPES.include?(target.symbol_type)
end
end
# Read the project from the disk to ensure that it is up to date as
# other TargetIntegrators might have modified it.
#
# @return [Project]
#
def user_project
target.user_project
end
# @return [Specification::Consumer] the consumer for the specifications.
#
def spec_consumers
@spec_consumers ||= target.pod_targets.map(&:file_accessors).flatten.map(&:spec_consumer)
end
# @return [String] the message that should be displayed for the target
# integration.
#
def integration_message
"Integrating target `#{target.name}` " \
"(#{UI.path target.user_project_path} project)"
end
end
end
end
end