blob: ce9c56eb2d6c4f49d59940c041d003005626d21a [file] [log] [blame]
require 'xcodeproj/workspace'
require 'xcodeproj/project'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/array/conversions'
module Pod
class Installer
# The {UserProjectIntegrator} integrates the libraries generated by
# TargetDefinitions of the {Podfile} with their correspondent user
# projects.
#
class UserProjectIntegrator
autoload :TargetIntegrator, 'cocoapods/installer/user_project_integrator/target_integrator'
# @return [Podfile] the podfile that should be integrated with the user
# projects.
#
attr_reader :podfile
# @return [Project] the pods project which contains the libraries to
# integrate.
#
# attr_reader :pods_project
attr_reader :sandbox
# @return [Pathname] the path of the installation.
#
# @todo This is only used to compute the workspace path in case that it
# should be inferred by the project. If the workspace should be in
# the same dir of the project, this could be removed.
#
attr_reader :installation_root
# @return [Array<AggregateTarget>] the targets represented in the Podfile.
#
attr_reader :targets
# Init a new UserProjectIntegrator
#
# @param [Podfile] podfile @see #podfile
# @param [Sandbox] sandbox @see #sandbox
# @param [Pathname] installation_root @see #installation_root
# @param [Array<AggregateTarget>] targets @see #targets
#
# @todo Too many initialization arguments
#
def initialize(podfile, sandbox, installation_root, targets)
@podfile = podfile
@sandbox = sandbox
@installation_root = installation_root
@targets = targets
end
# Integrates the user projects associated with the {TargetDefinitions}
# with the Pods project and its products.
#
# @return [void]
#
def integrate!
create_workspace
integrate_user_targets
warn_about_xcconfig_overrides
save_projects
end
#-----------------------------------------------------------------------#
private
# @!group Integration steps
# Creates and saved the workspace containing the Pods project and the
# user projects, if needed.
#
# @note If the workspace already contains the projects it is not saved
# to avoid Xcode from displaying the revert dialog: `Do you want to
# keep the Xcode version or revert to the version on disk?`
#
# @return [void]
#
def create_workspace
all_projects = user_project_paths.sort.push(sandbox.project_path).uniq
file_references = all_projects.map do |path|
relative_path = path.relative_path_from(workspace_path.dirname).to_s
Xcodeproj::Workspace::FileReference.new(relative_path, 'group')
end
if workspace_path.exist?
workspace = Xcodeproj::Workspace.new_from_xcworkspace(workspace_path)
new_file_references = file_references - workspace.file_references
unless new_file_references.empty?
new_file_references.each { |fr| workspace << fr }
workspace.save_as(workspace_path)
end
else
UI.notice "Please close any current Xcode sessions and use `#{workspace_path.basename}` for this project from now on."
workspace = Xcodeproj::Workspace.new(*file_references)
workspace.save_as(workspace_path)
end
end
# Integrates the targets of the user projects with the libraries
# generated from the {Podfile}.
#
# @note {TargetDefinition} without dependencies are skipped prevent
# creating empty libraries for targets definitions which are only
# wrappers for others.
#
# @return [void]
#
def integrate_user_targets
target_integrators = targets_to_integrate.sort_by(&:name).map do |target|
TargetIntegrator.new(target)
end
Config.instance.with_changes(:silent => true) do
deintegrator = Deintegrator.new
all_project_targets = user_projects.flat_map(&:native_targets).uniq
all_native_targets = targets_to_integrate.flat_map(&:user_targets).uniq
targets_to_deintegrate = all_project_targets - all_native_targets
targets_to_deintegrate.each do |target|
deintegrator.deintegrate_target(target)
end
end
target_integrators.each(&:integrate!)
end
# Save all user projects.
#
# @return [void]
#
def save_projects
user_projects.each do |project|
if project.dirty?
project.save
else
# There is a bug in Xcode where the process of deleting and
# re-creating the xcconfig files used in the build
# configuration cause building the user project to fail until
# Xcode is relaunched.
#
# Touching/saving the project causes Xcode to reload these.
#
# https://github.com/CocoaPods/CocoaPods/issues/2665
FileUtils.touch(project.path + 'project.pbxproj')
end
end
end
IGNORED_KEYS = %w(CODE_SIGN_IDENTITY).freeze
INHERITED_FLAGS = %w($(inherited) ${inherited}).freeze
# Checks whether the settings of the CocoaPods generated xcconfig are
# overridden by the build configuration of a target and prints a
# warning to inform the user if needed.
#
def warn_about_xcconfig_overrides
targets.each do |aggregate_target|
aggregate_target.user_targets.each do |user_target|
user_target.build_configurations.each do |config|
xcconfig = aggregate_target.xcconfigs[config.name]
if xcconfig
(xcconfig.to_hash.keys - IGNORED_KEYS).each do |key|
target_values = config.build_settings[key]
if target_values &&
!INHERITED_FLAGS.any? { |flag| target_values.include?(flag) }
print_override_warning(aggregate_target, user_target, config, key)
end
end
end
end
end
end
end
private
# @!group Private Helpers
#-----------------------------------------------------------------------#
# @return [Pathname] the path where the workspace containing the Pods
# project and the user projects should be saved.
#
def workspace_path
if podfile.workspace_path
declared_path = podfile.workspace_path
path_with_ext = File.extname(declared_path) == '.xcworkspace' ? declared_path : "#{declared_path}.xcworkspace"
podfile_dir = File.dirname(podfile.defined_in_file || '')
absolute_path = File.expand_path(path_with_ext, podfile_dir)
Pathname.new(absolute_path)
elsif user_project_paths.count == 1
project = user_project_paths.first.basename('.xcodeproj')
installation_root + "#{project}.xcworkspace"
else
raise Informative, 'Could not automatically select an Xcode ' \
"workspace. Specify one in your Podfile like so:\n\n" \
" workspace 'path/to/Workspace.xcworkspace'\n"
end
end
# @return [Array<Pathname>] the paths of all the user projects referenced
# by the target definitions.
#
# @note Empty target definitions are ignored.
#
def user_project_paths
targets.map(&:user_project_path).compact.uniq
end
def user_projects
targets.map(&:user_project).compact.uniq
end
def targets_to_integrate
targets
end
# Prints a warning informing the user that a build configuration of
# the integrated target is overriding the CocoaPods build settings.
#
# @param [Target::AggregateTarget] aggregate_target
# The umbrella target.
#
# @param [XcodeProj::PBXNativeTarget] user_target
# The native target.
#
# @param [Xcodeproj::XCBuildConfiguration] config
# The build configuration.
#
# @param [String] key
# The key of the overridden build setting.
#
def print_override_warning(aggregate_target, user_target, config, key)
actions = [
'Use the `$(inherited)` flag, or',
'Remove the build settings from the target.',
]
message = "The `#{user_target.name} [#{config.name}]` " \
"target overrides the `#{key}` build setting defined in " \
"`#{aggregate_target.xcconfig_relative_path(config.name)}'. " \
'This can lead to problems with the CocoaPods installation'
UI.warn(message, actions)
end
#-----------------------------------------------------------------------#
end
end
end