Commit dec60494 authored by Fabio Pelosin's avatar Fabio Pelosin

[WIP] Installer clean up.

parent 80997638
...@@ -41,9 +41,8 @@ module Pod ...@@ -41,9 +41,8 @@ module Pod
def run_install_with_update(update) def run_install_with_update(update)
sandbox = Sandbox.new(config.project_pods_root) sandbox = Sandbox.new(config.project_pods_root)
resolver = Resolver.new(config.podfile, config.lockfile, sandbox) installer = Installer.new(sandbox, config.podfile, config.lockfile)
resolver.update_mode = update installer.install!
Installer.new(resolver).install!
end end
def run def run
......
...@@ -5,16 +5,56 @@ module Pod ...@@ -5,16 +5,56 @@ module Pod
# integrates the user project so the Pods libraries can be used out of the # integrates the user project so the Pods libraries can be used out of the
# box. # box.
# #
# The installer is capable of doing incremental updates to an existing Pod
# installation.
#
# The installer gets the information that it needs mainly from 3 files:
#
# - Podfile: The specification written by the user that contains
# information about targets and Pods.
# - Podfile.lock: Contains information about the pods that were previously
# installed and in concert with the Podfile provides information about
# which specific version of a Pod should be installed. This file is
# ignored in update mode.
# - Pods.lock: A file contained in the Pods folder that keeps track
# of the pods installed in the local machine. This files is used once
# the exact versions of the Pods has been computed to detect if that
# version is already installed. This file is not intended to be kept
# under source control and is a copy of the Podfile.lock.
#
# Once completed the installer should produce the following file structure:
#
# Pods
# |
# +-- Headers
# | +-- Build
# | | +-- [Pod Name]
# | +-- Public
# | +-- [Pod Name]
# |
# +-- Sources
# | +-- [Pod Name]
# |
# +-- Specifications
# |
# +-- Target Support Files
# | +-- [Target Name]
# | +-- Acknowledgements.markdown
# | +-- Acknowledgements.plist
# | +-- Pods.xcconfig
# | +-- Pods-prefix.pch
# | +-- PodsDummy_Pods.m
# |
# +-- Pods.lock
# |
# +-- Pods.xcodeproj
#
class Installer class Installer
autoload :TargetInstaller, 'cocoapods/installer/target_installer' autoload :TargetInstaller, 'cocoapods/installer/target_installer'
autoload :UserProjectIntegrator, 'cocoapods/installer/user_project_integrator' autoload :UserProjectIntegrator, 'cocoapods/installer/user_project_integrator'
include Config::Mixin include Config::Mixin
# @return [Resolver] The resolver used by the installer.
#
attr_reader :resolver
# @return [Sandbox] The sandbox where to install the Pods. # @return [Sandbox] The sandbox where to install the Pods.
# #
attr_reader :sandbox attr_reader :sandbox
...@@ -29,67 +69,121 @@ module Pod ...@@ -29,67 +69,121 @@ module Pod
# #
attr_reader :lockfile attr_reader :lockfile
# TODO: The installer should receive the Podfile, the Lockfile, and the # @return [Bool] Whether the installer is in update mode. In update
# sandbox. It shouldn't get those values from the resolver, but it should # mode the contents of the Lockfile are not taken into
# create the resolver itself. # account for deciding what Pods to install.
# #
def initialize(resolver) attr_reader :update_mode
@resolver = resolver
@podfile = resolver.podfile # @param [Sandbox] sandbox @see sandbox
@sandbox = resolver.sandbox # @param [Podfile] podfile @see podfile
# @param [Lockfile] lockfile @see lockfile
# @param [Bool] update_mode @see update_mode
#
def initialize(sandbox, podfile, lockfile = nil, update_mode = false)
@sandbox = sandbox
@podfile = podfile
@lockfile = lockfile
@update_mode = update_mode
end end
# @return [void] The installation process of CocoaPods is mostly linear # @return [void] The installation process of is mostly linear with few
# with very few minor exceptions: # minor complications to keep in mind:
#
# - The stored podspecs need to be cleaned before the resolution step
# otherwise the sandbox might return an old podspec and not download
# the new one from an external source.
# - The resolver might trigger the download of Pods from external sources
# necessary to retrieve their podspec (unless it is instructed not to
# do it).
# #
# - Pods from external sources might be already downloaded if it is # @note The order of the steps is very important and should be changed
# necessary to retrieve their podspec. # carefully.
#
# TODO:
# #
def install! def install!
detect_podfile_changes # TODO: prepare_for_legacy_compatibility
perform_global_cleaning compare_podfile_and_lockfile
perform_pod_specific_cleaning
clean_global_support_files
clean_removed_pods
clean_pods_to_install
update_repositories_if_needed
generate_locked_dependencies
resolve_dependencies resolve_dependencies
# TODO: move to perform_pod_specific_cleaning # TODO: detect_installed_versions
UI.section "Removing deleted dependencies" do create_local_pods
remove_deleted_dependencies! detect_pods_to_install
end unless resolver.removed_pods.empty? install_dependencies
prepare_pods_project
install_dependencies!
generate_support_files generate_support_files
write_lockfile
# TODO: write_sandbox_lockfile
integrate_user_project integrate_user_project
end end
# @return [void] the
#
def dry_run
end
# @!group Prepare for legacy compatibility
# @return [void] In this step we prepare the Pods folder in order to be
# compatible with the most recent version of CocoaPods.
#
# @note This step should be removed by version 1.0.
#
def prepare_for_legacy_compatibility
# move_target_support_files_if_needed
# copy_lock_file_to_Pods_lock_if_needed
# move_Local_Podspecs_to_Podspecs_if_needed
# move_pods_to_sources_folder_if_needed
end
# @!group Detect Podfile changes step # @!group Detect Podfile changes step
# @return [Hash{Symbol => Array<Spec>}] The pods grouped by a symbol # @return [Hash{Symbol => Array<Spec>}] The name of the pods directly
# indicating the state (added, changed, removed, unchanged) as identified # specified in the Podfile grouped by a symbol representing their state
# by the {Lockfile}. # (added, changed, removed, unchanged) as identified by the {Lockfile}.
# #
attr_reader :pods_by_state attr_reader :pods_by_state
# @return [void] Computes the pods that need to be installed. # @return [void] In this step the podfile is compared with the lockfile in
# order to detect which dependencies should be locked.
#
# #TODO: If there is not lockfile all the Pods should be marked as added.
# #TODO: This should use the Pods.lock file because they are used by the
# to detect what needs to be installed.
# #
def detect_podfile_changes def compare_podfile_and_lockfile
if lockfile if lockfile
UI.section "Finding added, modified or removed dependencies:" do UI.section "Finding added, modified or removed dependencies:" do
@pods_by_state = @lockfile.detect_changes_with_podfile(podfile) @pods_by_state = lockfile.detect_changes_with_podfile(podfile)
print_pods_states_list display_dependencies_state
@unchanged_pods = (lockfile.pods_names - pods_by_state[:added] - pods_by_state[:changed] - pods_by_state[:removed]).uniq
end end
else else
@pods_by_state = {} @pods_by_state = {}
@unchanged_pods = []
end end
end end
# @return [void] Outputs a lists of the pods by state. # @return [void] Displays the state of each dependency.
# #
def print_pods_states_list def display_dependencies_state
return if config.verbose? return unless config.verbose?
marks = {:added => "A".green, :changed => "M".yellow, :removed => "R".red, :unchanged => "-" } marks = { :added => "A".green,
:changed => "M".yellow,
:removed => "R".red,
:unchanged => "-" }
pods_by_state.each do |symbol, pod_names| pods_by_state.each do |symbol, pod_names|
pod_names.each do |pod_name| pod_names.each do |pod_name|
UI.message("#{marks[symbol]} #{pod_name}", '',2) UI.message("#{marks[symbol]} #{pod_name}", '',2)
...@@ -97,25 +191,91 @@ module Pod ...@@ -97,25 +191,91 @@ module Pod
end end
end end
# @!group Cleaning steps # @!group Cleaning steps
def perform_global_cleaning # @return [void] In this step we clean all the folders that will be
@sandbox.prepare_for_install # regenerated from scratch and any file which might not be overwritten.
#
# @TODO: Clean the podspecs of all the pods that aren't unchanged so the
# resolution process doesn't get confused by them.
#
def clean_global_support_files
sandbox.prepare_for_install
end end
def perform_pod_specific_cleaning # @return [void] In this step we clean all the files related to the removed
# TODO: clean the headers of only the pods to install # Pods.
#
# @TODO: Use the local pod implode.
# @TODO: [#534] Clean all the Pods folder that are not unchanged?
#
def clean_removed_pods
UI.section "Removing deleted dependencies" do
pods_by_state[:removed].each do |pod_name|
UI.section("Removing #{pod_name}", "-> ".red) do
path = sandbox.root + pod_name
path.rmtree if path.exist?
end
end
end unless pods_by_state[:removed].empty?
end end
# Resolves the dependencies with the resolver # @return [void] In this step we clean the files of the Pods that will be
# installed. We clean the files that might affect the resolution process
# and the files that might not be overwritten.
# #
def resolve_dependencies # @TODO: [#247] Clean the headers of only the pods to install.
#TODO: prepare the resolver #
#TODO: lock the dependencies def clean_pods_to_install
UI.section "Resolving dependencies of #{UI.path @podfile.defined_in_file}" do end
@specs_by_target = @resolver.resolve
# @!group Generate locked dependencies step
# @return [void] Lazily updates the source repositories. The update is
# triggered if:
# - There are pods that changed in the Podfile.
# - The lockfile is missing.
# - The installer is in update_mode.
#
# TODO: Remove the lockfile condition once compare_podfile_and_lockfile
# is updated.
#
def update_repositories_if_needed
return if config.skip_repo_update?
changed_pods = (pods_by_state[:added] + pods_by_state[:changed])
UI.section 'Updating spec repositories' do
Command::Repo.new(Command::ARGV.new(["update"])).run
end if !lockfile || !changed_pods.empty? || update_mode
end end
# @!group Generate locked dependencies step
# @return [Array<Specification>] All dependencies that have been resolved.
#
attr_reader :locked_dependencies
# @return [void] In this step we generate the dependencies of necessary to
# prevent the resolver from updating the pods which are in unchanged
# state. The Podfile is compared to the Podfile.lock to detect what
# version of a dependency should be locked.
#
def generate_locked_dependencies
if update_mode
@locked_dependencies = []
else
@locked_dependencies = pods_by_state[:unchanged].map do |pod_name|
lockfile.dependency_for_installed_pod_named(pod_name)
end end
end
end
# @!group Resolution steps
# @return [Hash{Podfile::TargetDefinition => Array<Spec>}] # @return [Hash{Podfile::TargetDefinition => Array<Spec>}]
# The specifications grouped by target as identified in # The specifications grouped by target as identified in
...@@ -123,109 +283,158 @@ module Pod ...@@ -123,109 +283,158 @@ module Pod
# #
attr_reader :specs_by_target attr_reader :specs_by_target
# @return [Array<Specification>] All dependencies that have been resolved.
#
attr_reader :specifications
def prepare_pods_project # @return [void] Converts the Podfile in a list of specifications grouped
# by target.
end
# Install the Pods. If the resolver indicated that a Pod should be installed
# and it exits, it is removed an then reinstalled. In any case if the Pod
# doesn't exits it is installed.
# #
# @return [void] # In update mode the specs from external sources are always downloaded.
# #
def install_dependencies! def resolve_dependencies
UI.section "Downloading dependencies" do UI.section "Resolving dependencies of #{UI.path podfile.defined_in_file}" do
pods.sort_by { |pod| pod.top_specification.name.downcase }.each do |pod| resolver = Resolver.new(sandbox, podfile, locked_dependencies)
should_install = @resolver.should_install?(pod.top_specification.name) || !pod.exists? resolver.update_external_specs = update_mode
if should_install @specs_by_target = resolver.resolve
UI.section("Installing #{pod}".green, "-> ".green) do @specifications = specs_by_target.values.flatten
unless pod.downloaded?
pod.implode
download_pod(pod)
end end
# The docs need to be generated before cleaning because the
# documentation is created for all the subspecs.
generate_docs(pod)
# Here we clean pod's that just have been downloaded or have been
# pre-downloaded in AbstractExternalSource#specification_from_sandbox.
pod.clean! if config.clean?
end end
else
UI.section("Using #{pod}", "-> ".green)
# @!group Detect Pods to install step
# @return [Array<String>] The names of the Pods that should be installed.
#
attr_reader :pods_to_install
# @return [<void>] In this step the pods to install are detected.
# The pods to install are identified as the Pods that don't exist in the
# sandbox or the Pods whose version differs from the one of the lockfile.
#
# In update mode specs originating from external dependencies and or from
# head sources are always reinstalled.
#
# TODO: Decide a how the Lockfile should report versions.
# TODO: [#534] Detect if the folder of a Pod is empty.
#
def detect_pods_to_install
changed_pods_names = []
if lockfile
changed_pods = pods.select do |pod|
pod.top_specification.version != lockfile.pods_versions[pod.name]
end end
if update_mode
changed_pods_names += pods.select do |pods|
pod.top_specification.version.head? ||
resolver.pods_from_external_sources.include?(pod.name)
end end
end end
changed_pods_names += @pods_by_state[:added] + @pods_by_state[:changed]
else
changed_pods = pods
end end
def generate_support_files not_existing_pods = pods.reject { |pod| pod.exists? }
UI.section "Generating support files" do @pods_to_install = (changed_pods + not_existing_pods).uniq
UI.message "- Running pre install hooks" do
run_pre_install_hooks
end end
UI.message"- Installing targets" do
generate_target_support_files
end
UI.message "- Running post install hooks" do
# Post install hooks run _before_ saving of project, so that they can alter it before saving.
run_post_install_hooks
end
UI.message "- Writing Xcode project file to #{UI.path @sandbox.project_path}" do # @!group Install step
project.save_as(@sandbox.project_path)
end
UI.message "- Writing lockfile in #{UI.path config.project_lockfile}" do # @return [Hash{Podfile::TargetDefinition => Array<LocalPod>}]
@lockfile = Lockfile.generate(@podfile, specs_by_target.values.flatten) #
@lockfile.write_to_disk(config.project_lockfile) attr_reader :pods_by_target
# @return [Array<LocalPod>] A list of LocalPod instances for each
# dependency sorted by name.
# (that is not a download-only one?)
attr_reader :pods
# @return [void] In this step the specifications obtained by the resolver
# are converted in local pods. The LocalPod class is responsible to
# handle the concrete representation of a specification a sandbox.
#
# @TODO: [#535] Pods should be accumulated per Target, also in the Local
# Pod class. The Local Pod class should have a method to add itself
# to a given project so it can use the sources of all the activated
# podspecs across all targets. Also cleaning should take into
# account that.
#
def create_local_pods
@pods_by_target = {}
specs_by_target.each do |target_definition, specs|
@pods_by_target[target_definition] = specs.map do |spec|
if spec.local?
sandbox.locally_sourced_pod_for_spec(spec, target_definition.platform)
else
sandbox.local_pod_for_spec(spec, target_definition.platform)
end end
end.uniq.compact
end end
@pods = pods_by_target.values.flatten.uniq.sort_by { |pod| pod.name.downcase }
end end
def integrate_user_project
UserProjectIntegrator.new(@podfile).integrate! if config.integrate_targets?
end
# @!group Supporting operations
def project # @!group Install step
return @project if @project
@project = Pod::Project.new # @return [void] Install the Pods. If the resolver indicated that a Pod
@project.user_build_configurations = @podfile.user_build_configurations # should be installed and it exits, it is removed an then reinstalled. In
# any case if the Pod doesn't exits it is installed.
#
def install_dependencies
UI.section "Downloading dependencies" do
pods.each do |pod| pods.each do |pod|
# Add all source files to the project grouped by pod if pods_to_install.include?(pod)
pod.relative_source_files_by_spec.each do |spec, paths| UI.section("Installing #{pod}".green, "-> ".green) do
parent_group = pod.local? ? @project.local_pods : @project.pods install_local_pod(pod)
group = @project.add_spec_group(spec.name, parent_group)
paths.each do |path|
group.files.new('path' => path.to_s)
end end
else
UI.section("Using #{pod}", "-> ".green)
end end
end end
# Add a group to hold all the target support files
@project.main_group.groups.new('name' => 'Targets Support Files')
@project
end end
def target_installers
@target_installers ||= @podfile.target_definitions.values.map do |definition|
TargetInstaller.new(@podfile, project, definition) unless definition.empty?
end.compact
end end
# @return [void] Downloads, clean and generates the documentation of a pod.
#
# @note The docs need to be generated before cleaning because the
# documentation is created for all the subspecs.
#
# @note In this step we clean also the Pods that have been pre-downloaded
# in AbstractExternalSource#specification_from_sandbox.
#
# TODO: [#529] Podspecs should not be preserved anymore to prevent user
# confusion. Currently we are copying the ones form external sources
# in `Local Podspecs` and this feature is not needed anymore.
# I think that copying all the used podspecs would be helpful for
# debugging.
#
def install_local_pod(pod)
unless pod.downloaded?
pod.implode
download_pod(pod)
end
generate_docs_if_needed(pod)
pod.clean! if config.clean?
end
# @return [void] Downloads a Pod forcing the `bleeding edge' version if
# requested.
#
def download_pod(pod) def download_pod(pod)
downloader = Downloader.for_pod(pod) downloader = Downloader.for_pod(pod)
# Force the `bleeding edge' version if necessary.
if pod.top_specification.version.head? if pod.top_specification.version.head?
if downloader.respond_to?(:download_head) if downloader.respond_to?(:download_head)
downloader.download_head downloader.download_head
else else
raise Informative, "The downloader of class `#{downloader.class.name}' does not support the `:head' option." raise Informative,
"The downloader of class `#{downloader.class.name}' does not" \
"support the `:head' option."
end end
else else
downloader.download downloader.download
...@@ -233,8 +442,10 @@ module Pod ...@@ -233,8 +442,10 @@ module Pod
pod.downloaded = true pod.downloaded = true
end end
#TODO: move to generator ? # @return [void] Generates the documentation of a Pod unless it exists
def generate_docs(pod) # for a given version.
#
def generate_docs_if_needed(pod)
doc_generator = Generator::Documentation.new(pod) doc_generator = Generator::Documentation.new(pod)
if ( config.generate_docs? && !doc_generator.already_installed? ) if ( config.generate_docs? && !doc_generator.already_installed? )
UI.section " > Installing documentation" UI.section " > Installing documentation"
...@@ -244,20 +455,77 @@ module Pod ...@@ -244,20 +455,77 @@ module Pod
end end
end end
# @TODO: use the local pod implode
# @!group Generate Pods project and support files step
# @return [void] Creates and populates the targets of the pods project.
# #
def remove_deleted_dependencies! # @note Post install hooks run _before_ saving of project, so that they can
resolver.removed_pods.each do |pod_name| # alter it before saving.
UI.section("Removing #{pod_name}", "-> ".red) do #
path = sandbox.root + pod_name def generate_support_files
path.rmtree if path.exist? UI.section "Generating support files" do
prepare_pods_project
add_source_files_to_pods_project
run_pre_install_hooks
generate_target_support_files
run_post_install_hooks
write_pod_project
end
end end
# @return [Project] The Pods project.
#
attr_reader :pods_project
# @return [void] In this step we create the Pods project from scratch if it
# doesn't exists. If the Pods project exists instead we clean it and
# prepare it for installation.
#
def prepare_pods_project
UI.message "- Creating Pods project" do
@pods_project = Pod::Project.new
pods_project.user_build_configurations = podfile.user_build_configurations
pods_project.main_group.groups.new('name' => 'Targets Support Files')
end end
end end
# @return [void] In this step we add the source files of the Pods to the
# Pods project. The source files are grouped by Pod and in turn by subspec
# (recursively). Pods are generally added to the Pods group. However, if
# they are local they are added to the Local Pods group.
#
# @TODO [#143] This step is quite slow and should be made incremental by
# modifying only the files of the changed pods. Xcodeproj deletion
# and sorting of folders is required.
#
def add_source_files_to_pods_project
UI.message "- Adding source files to Pods project" do
pods.each do |pod|
pod.relative_source_files_by_spec.each do |spec, paths|
if pod.local?
parent_group = pods_project.local_pods
else
parent_group = pods_project.pods
end
group = pods_project.add_spec_group(spec.name, parent_group)
paths.each do |path|
group.files.new('path' => path.to_s)
end
end
end
end
end
def target_installers
@target_installers ||= podfile.target_definitions.values.map do |definition|
TargetInstaller.new(podfile, pods_project, definition) unless definition.empty?
end.compact
end
def run_pre_install_hooks def run_pre_install_hooks
UI.message "- Running pre install hooks" do
pods_by_target.each do |target_definition, pods| pods_by_target.each do |target_definition, pods|
pods.each do |pod| pods.each do |pod|
pod.top_specification.pre_install(pod, target_definition) pod.top_specification.pre_install(pod, target_definition)
...@@ -265,10 +533,12 @@ module Pod ...@@ -265,10 +533,12 @@ module Pod
end end
@podfile.pre_install!(self) @podfile.pre_install!(self)
end end
end
def run_post_install_hooks def run_post_install_hooks
# we loop over target installers instead of pods, because we yield the target installer UI.message "- Running post install hooks" do
# to the spec post install hook. # we loop over target installers instead of pods, because we yield the
# target installer to the spec post install hook.
target_installers.each do |target_installer| target_installers.each do |target_installer|
specs_by_target[target_installer.target_definition].each do |spec| specs_by_target[target_installer.target_definition].each do |spec|
spec.post_install(target_installer) spec.post_install(target_installer)
...@@ -276,17 +546,20 @@ module Pod ...@@ -276,17 +546,20 @@ module Pod
end end
@podfile.post_install!(self) @podfile.post_install!(self)
end end
end
def generate_target_support_files def generate_target_support_files
UI.message"- Installing targets" do
target_installers.each do |target_installer| target_installers.each do |target_installer|
pods_for_target = pods_by_target[target_installer.target_definition] pods_for_target = pods_by_target[target_installer.target_definition]
target_installer.install!(pods_for_target, @sandbox) target_installer.install!(pods_for_target, sandbox)
acknowledgements_path = target_installer.target_definition.acknowledgements_path acknowledgements_path = target_installer.target_definition.acknowledgements_path
Generator::Acknowledgements.new(target_installer.target_definition, Generator::Acknowledgements.new(target_installer.target_definition,
pods_for_target).save_as(acknowledgements_path) pods_for_target).save_as(acknowledgements_path)
generate_dummy_source(target_installer) generate_dummy_source(target_installer)
end end
end end
end
def generate_dummy_source(target_installer) def generate_dummy_source(target_installer)
class_name_identifier = target_installer.target_definition.label class_name_identifier = target_installer.target_definition.label
...@@ -295,37 +568,46 @@ module Pod ...@@ -295,37 +568,46 @@ module Pod
pathname = Pathname.new(sandbox.root + filename) pathname = Pathname.new(sandbox.root + filename)
dummy_source.save_as(pathname) dummy_source.save_as(pathname)
project_file = project.files.new('path' => filename) project_file = pods_project.files.new('path' => filename)
project.group("Targets Support Files") << project_file pods_project.group("Targets Support Files") << project_file
target_installer.target.source_build_phases.first << project_file target_installer.target.source_build_phases.first << project_file
end end
# @return [Array<Specification>] All dependencies that have been resolved. def write_pod_project
def specifications UI.message "- Writing Xcode project file to #{UI.path @sandbox.project_path}" do
specs_by_target.values.flatten pods_project.save_as(@sandbox.project_path)
end end
# @return [Array<LocalPod>] A list of LocalPod instances for each
# dependency that is not a download-only one.
def pods
pods_by_target.values.flatten.uniq
end end
def pods_by_target
@pods_by_spec = {}
result = {} # @!group Lockfile related steps
specs_by_target.each do |target_definition, specs|
@pods_by_spec[target_definition.platform] = {} def write_lockfile
result[target_definition] = specs.map do |spec| UI.message "- Writing Lockfile in #{UI.path config.project_lockfile}" do
if spec.local? @lockfile = Lockfile.generate(podfile, specs_by_target.values.flatten)
@sandbox.locally_sourced_pod_for_spec(spec, target_definition.platform) @lockfile.write_to_disk(config.project_lockfile)
else
@sandbox.local_pod_for_spec(spec, target_definition.platform)
end end
end.uniq.compact
end end
result
# @TODO: [#552] Implement
#
def write_sandbox_lockfile
end
# @!group Integrate user project step
# @return [void] In this step the user project is integrated. The Pods
# libraries are added, the build script are added, and the xcconfig files
# are set.
#
# @TODO: [#397] The libraries should be cleaned and the re-added on every
# install. Maybe a clean_user_project phase should be added.
#
def integrate_user_project
UserProjectIntegrator.new(podfile).integrate! if config.integrate_targets?
end end
end end
end end
...@@ -50,8 +50,8 @@ module Pod ...@@ -50,8 +50,8 @@ module Pod
end end
# @return [Hash{String => Hash}] A hash where the name of the pods are # @return [Hash{String => Hash}] A hash where the name of the pods are
# the keys and the values are the parameters of an {AbstractExternalSource} # the keys and the values are the parameters of an
# of the dependency that required the pod. # {AbstractExternalSource} of the dependency that required the pod.
# #
def external_sources def external_sources
@external_sources ||= to_hash["EXTERNAL SOURCES"] || {} @external_sources ||= to_hash["EXTERNAL SOURCES"] || {}
...@@ -67,8 +67,8 @@ module Pod ...@@ -67,8 +67,8 @@ module Pod
end end
# @return [Hash{String => Version}] A Hash containing the name # @return [Hash{String => Version}] A Hash containing the name
# of the installed Pods as the keys and their corresponding {Version} # of the installed Pods (top spec name) as the keys and their
# as the values. # corresponding {Version} as the values.
# #
def pods_versions def pods_versions
unless @pods_versions unless @pods_versions
...@@ -77,6 +77,7 @@ module Pod ...@@ -77,6 +77,7 @@ module Pod
pod = pod.keys.first unless pod.is_a?(String) pod = pod.keys.first unless pod.is_a?(String)
name, version = name_and_version_for_pod(pod) name, version = name_and_version_for_pod(pod)
@pods_versions[name] = version @pods_versions[name] = version
@pods_versions[name.split('/').first] = version
end end
end end
@pods_versions @pods_versions
......
require 'colored'
module Pod module Pod
class Resolver
include Config::Mixin
# @return [Bool] Whether the resolver should find the pods to install or # The resolver is responsible of generating a list of specifications grouped
# the pods to update. # by target for a given Podfile.
# #
attr_accessor :update_mode # Its current implementation is naive, in the sense that it can't do full
# automatic resolves like Bundler:
# @return [Bool] Whether the resolver should update the external specs
# in the resolution process.
# #
attr_accessor :update_external_specs # http://patshaughnessy.net/2011/9/24/how-does-bundler-bundle
# @return [Podfile] The Podfile used by the resolver.
# #
attr_reader :podfile # Another important aspect to keep in mind of the current implementation
# is that the order of the dependencies matters.
# @return [Lockfile] The Lockfile used by the resolver.
# #
attr_reader :lockfile class Resolver
include Config::Mixin
# @return [Sandbox] The Sandbox used by the resolver to find external # @return [Sandbox] The Sandbox used by the resolver to find external
# dependencies. # dependencies.
# #
attr_reader :sandbox attr_reader :sandbox
# @return [Array<Strings>] The name of the pods that have an # @return [Podfile] The Podfile used by the resolver.
# external source.
# #
attr_reader :pods_from_external_sources attr_reader :podfile
# @return [Array<Set>] A cache of the sets used to resolve the dependencies. # @return [Array<Dependency>] The list of dependencies locked to a specific
# version.
# #
attr_reader :cached_sets attr_reader :locked_dependencies
# @return [Source::Aggregate] A cache of the sources needed to find the # @return [Bool] Whether the resolver should update the external specs
# podspecs. # in the resolution process. This option is used for detecting changes
# in with the Podfile without affecting the existing Pods installation
# (see `pod outdated`).
# #
attr_reader :cached_sources # @TODO: This implementation is not clean, because if the spec doesn't
# exists the sandbox will actually download it and result modified.
#
attr_accessor :update_external_specs
def initialize(sandbox, podfile, locked_dependencies)
@sandbox = sandbox
@podfile = podfile
@locked_dependencies = locked_dependencies
end
# @return [Hash{Podfile::TargetDefinition => Array<Specification>}] # @return [Hash{Podfile::TargetDefinition => Array<Specification>}]
# Returns the resolved specifications grouped by target. # Returns the resolved specifications grouped by target.
# #
attr_reader :specs_by_target attr_reader :specs_by_target
def initialize(podfile, lockfile, sandbox) # @return [Array<Specification>] All The specifications loaded by the
@podfile = podfile # resolver.
@lockfile = lockfile #
@sandbox = sandbox def specs
@update_external_specs = true @cached_specs.values.uniq
@cached_sets = {}
@cached_sources = Source::Aggregate.new
end end
# Identifies the specifications that should be installed according whether # @return [Array<Strings>] The name of the pods that have an
# the resolver is in update mode or not. # external source.
#
# @TODO: Add an attribute to the specification class?
# #
# @return [Hash{Podfile::TargetDefinition => Array<Specification>}] specs_by_target attr_reader :pods_from_external_sources
# @return [Hash{TargetDefinition => Array<Specification>}] specs_by_target
# Identifies the specifications that should be installed according
# whether the resolver is in update mode or not.
# #
def resolve def resolve
@cached_sources = Source::Aggregate.new
@cached_sets = {}
@cached_specs = {} @cached_specs = {}
@specs_by_target = {} @specs_by_target = {}
@pods_from_external_sources = [] @pods_from_external_sources = []
@pods_to_lock = []
if @lockfile podfile.target_definitions.values.each do |target_definition|
@pods_by_state = @lockfile.detect_changes_with_podfile(podfile)
UI.section "Finding added, modified or removed dependencies:" do
marks = {:added => "A".green, :changed => "M".yellow, :removed => "R".red, :unchanged => "-" }
@pods_by_state.each do |symbol, pod_names|
pod_names.each do |pod_name|
UI.message("#{marks[symbol]} #{pod_name}", '',2)
end
end
end if config.verbose?
@pods_to_lock = (lockfile.pods_names - @pods_by_state[:added] - @pods_by_state[:changed] - @pods_by_state[:removed]).uniq
end
unless config.skip_repo_update?
UI.section 'Updating spec repositories' do
Command::Repo.new(Command::ARGV.new(["update"])).run
end if !@lockfile || !(@pods_by_state[:added] + @pods_by_state[:changed]).empty? || update_mode
end
@podfile.target_definitions.values.each do |target_definition|
UI.section "Resolving dependencies for target `#{target_definition.name}' (#{target_definition.platform})" do UI.section "Resolving dependencies for target `#{target_definition.name}' (#{target_definition.platform})" do
@loaded_specs = [] @loaded_specs = []
find_dependency_specs(@podfile, target_definition.dependencies, target_definition) find_dependency_specs(podfile, target_definition.dependencies, target_definition)
@specs_by_target[target_definition] = @cached_specs.values_at(*@loaded_specs).sort_by(&:name) @specs_by_target[target_definition] = @cached_specs.values_at(*@loaded_specs).sort_by(&:name)
end end
end end
...@@ -98,102 +86,45 @@ module Pod ...@@ -98,102 +86,45 @@ module Pod
@specs_by_target @specs_by_target
end end
# @return [Array<Specification>] The specifications loaded by the resolver. private
#
def specs
@cached_specs.values.uniq
end
# @return [Bool] Whether a pod should be installed/reinstalled.
#
def should_install?(name)
pods_to_install.include? name
end
# @return [Array<Strings>] The name of the pods that should be
# installed/reinstalled.
#
def pods_to_install
unless @pods_to_install
if lockfile
@pods_to_install = specs.select do |spec|
spec.version != lockfile.pods_versions[spec.pod_name]
end.map(&:name)
if update_mode
@pods_to_install += specs.select do |spec|
spec.version.head? || pods_from_external_sources.include?(spec.pod_name)
end.map(&:name)
end
@pods_to_install += @pods_by_state[:added] + @pods_by_state[:changed]
else
@pods_to_install = specs.map(&:name)
end
end
@pods_to_install
end
# @return [Array<Strings>] The name of the pods that were installed # @return [Array<Set>] A cache of the sets used to resolve the dependencies.
# but don't have any dependency anymore. The name of the Pods are
# stripped from subspecs.
# #
def removed_pods attr_reader :cached_sets
return [] unless lockfile
unless @removed_pods
previusly_installed = lockfile.pods_names.map { |pod_name| pod_name.split('/').first }
installed = specs.map { |spec| spec.name.split('/').first }
@removed_pods = previusly_installed - installed
end
@removed_pods
end
private
# @return [Set] The cached set for a given dependency. # @return [Source::Aggregate] A cache of the sources needed to find the
# podspecs.
# #
def find_cached_set(dependency, platform) attr_reader :cached_sources
set_name = dependency.name.split('/').first
@cached_sets[set_name] ||= begin
if dependency.specification
Specification::Set::External.new(dependency.specification)
elsif external_source = dependency.external_source
if update_mode && update_external_specs
# Always update external sources in update mode.
specification = external_source.specification_from_external(@sandbox, platform)
else
# Don't update external sources in install mode if not needed.
specification = external_source.specification_from_sandbox(@sandbox, platform)
end
set = Specification::Set::External.new(specification)
if dependency.subspec_dependency?
@cached_sets[dependency.top_level_spec_name] ||= set
end
set
else
@cached_sources.search(dependency)
end
end
end
# Resolves the dependencies of a specification and stores them in @cached_specs # @return [void] Resolves recursively the dependencies of a specification
# and stores them in @cached_specs
# #
# @param [Specification] dependent_specification # @param [Specification] dependent_specification
# The specification whose dependencies are being resolved.
#
# @param [Array<Dependency>] dependencies # @param [Array<Dependency>] dependencies
# @param [TargetDefinition] target_definition # The dependencies of the specification.
# #
# @return [void] # @param [TargetDefinition] target_definition
# The target definition that owns the specification.
# #
def find_dependency_specs(dependent_specification, dependencies, target_definition) def find_dependency_specs(dependent_specification, dependencies, target_definition)
dependencies.each do |dependency| dependencies.each do |dependency|
# Replace the dependency with a more specific one if the pod is already installed. # Replace the dependency with a more specific one if the pod is already
if !update_mode && @pods_to_lock.include?(dependency.name) # installed.
dependency = lockfile.dependency_for_installed_pod_named(dependency.name) # @TODO: check for compatibility?
end locked_dep = locked_dependencies.find { |locked| locked.name == dependency.name }
dependency = locked_dep if locked_dep
UI.message("- #{dependency}", '', 2) do UI.message("- #{dependency}", '', 2) do
set = find_cached_set(dependency, target_definition.platform) set = find_cached_set(dependency, target_definition.platform)
set.required_by(dependency, dependent_specification.to_s) set.required_by(dependency, dependent_specification.to_s)
# Ensure we don't resolve the same spec twice for one target # Ensure we don't resolve the same spec twice for one target
unless @loaded_specs.include?(dependency.name) if @loaded_specs.include?(dependency.name)
validate_platform(@cached_specs[dependency.name], target_definition)
else
spec = set.specification_by_name(dependency.name) spec = set.specification_by_name(dependency.name)
@pods_from_external_sources << spec.pod_name if dependency.external? @pods_from_external_sources << spec.pod_name if dependency.external?
@loaded_specs << spec.name @loaded_specs << spec.name
...@@ -202,20 +133,53 @@ module Pod ...@@ -202,20 +133,53 @@ module Pod
spec.activate_platform(target_definition.platform) spec.activate_platform(target_definition.platform)
spec.version.head = dependency.head? spec.version.head = dependency.head?
# And recursively load the dependencies of the spec. # And recursively load the dependencies of the spec.
find_dependency_specs(spec, spec.dependencies, target_definition) if spec.dependencies find_dependency_specs(spec, spec.dependencies, target_definition)
validate_platform(spec, target_definition)
end
end
end
end end
validate_platform(spec || @cached_specs[dependency.name], target_definition)
# @return [Set] The cached set for a given dependency.
#
# If the update_external_specs flag is activated the dependencies with
# external sources are always resolved against the remote. Otherwise the
# specification is retrieved from the sanbox that fetches the external
# source only if needed.
#
def find_cached_set(dependency, platform)
set_name = dependency.name.split('/').first
@cached_sets[set_name] ||= begin
if dependency.specification
Specification::Set::External.new(dependency.specification)
elsif external_source = dependency.external_source
if update_external_specs
spec = external_source.specification_from_external(sandbox, platform)
else
spec = external_source.specification_from_sandbox(sandbox, platform)
end
set = Specification::Set::External.new(spec)
if dependency.subspec_dependency?
@cached_sets[dependency.top_level_spec_name] ||= set
end
set
else
cached_sources.search(dependency)
end end
end end
end end
# Ensures that a spec is compatible with the platform of a target. # @return [void] Ensures that a spec is compatible with the platform of a
# target.
# #
# @raises If the spec is not supported by the target. # @raises If the spec is not supported by the target.
# #
def validate_platform(spec, target) def validate_platform(spec, target)
unless spec.available_platforms.any? { |platform| target.platform.supports?(platform) } unless spec.available_platforms.any? { |platform| target.platform.supports?(platform) }
raise Informative, "[!] The platform of the target `#{target.name}' (#{target.platform}) is not compatible with `#{spec}' which has a minimun requirement of #{spec.available_platforms.join(' - ')}.".red raise Informative, "The platform of the target `#{target.name}' "\
"(#{target.platform}) is not compatible with `#{spec}' which " \
"has a minimum requirement of #{spec.available_platforms.join(' - ')}."
end end
end end
end end
......
...@@ -8,30 +8,6 @@ module Pod ...@@ -8,30 +8,6 @@ module Pod
end end
describe "by default" do describe "by default" do
before do
podfile = Podfile.new do
platform :ios
xcodeproj 'MyProject'
pod 'JSONKit'
end
sandbox = Sandbox.new(fixture('integration'))
resolver = Resolver.new(podfile, nil, sandbox)
@xcconfig = Installer.new(resolver).target_installers.first.xcconfig.to_hash
end
it "sets the header search paths where installed Pod headers can be found" do
@xcconfig['ALWAYS_SEARCH_USER_PATHS'].should == 'YES'
end
it "configures the project to load all members that implement Objective-c classes or categories from the static library" do
@xcconfig['OTHER_LDFLAGS'].should == '-ObjC'
end
it "sets the PODS_ROOT build variable" do
@xcconfig['PODS_ROOT'].should.not == nil
end
it "generates a BridgeSupport metadata file from all the pod headers" do it "generates a BridgeSupport metadata file from all the pod headers" do
podfile = Podfile.new do podfile = Podfile.new do
platform :osx platform :osx
...@@ -39,8 +15,7 @@ module Pod ...@@ -39,8 +15,7 @@ module Pod
end end
sandbox = Sandbox.new(fixture('integration')) sandbox = Sandbox.new(fixture('integration'))
resolver = Resolver.new(podfile, nil, sandbox) installer = Installer.new(sandbox, podfile)
installer = Installer.new(resolver)
pods = installer.specifications.map do |spec| pods = installer.specifications.map do |spec|
LocalPod.new(spec, installer.sandbox, podfile.target_definitions[:default].platform) LocalPod.new(spec, installer.sandbox, podfile.target_definitions[:default].platform)
end end
...@@ -89,6 +64,34 @@ module Pod ...@@ -89,6 +64,34 @@ module Pod
end end
end end
describe "concerning xcconfig files generation" do
before do
podfile = Podfile.new do
platform :ios
xcodeproj 'MyProject'
pod 'JSONKit'
end
sandbox = Sandbox.new(fixture('integration'))
installer = Installer.new(sandbox, podfile)
@xcconfig = installer.target_installers.first.xcconfig.to_hash
end
it "sets the header search paths where installed Pod headers can be found" do
@xcconfig['ALWAYS_SEARCH_USER_PATHS'].should == 'YES'
end
it "configures the project to load all members that implement Objective-c classes or categories from the static library" do
@xcconfig['OTHER_LDFLAGS'].should == '-ObjC'
end
it "sets the PODS_ROOT build variable" do
@xcconfig['PODS_ROOT'].should.not == nil
end
end
describe "concerning multiple pods originating form the same spec" do describe "concerning multiple pods originating form the same spec" do
extend SpecHelper::Fixture extend SpecHelper::Fixture
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment