class Pod::Generate::Installer

Responsible for creating a workspace for a single specification, given a configuration and a generated podfile.

Constants

DEFAULT_XCODE_VERSION
XCODE_VERSION_TO_OBJECT_VERSION

Attributes

configuration[R]

@return [Configuration]

the configuration to use when installing
podfile[R]

@return [Podfile]

the podfile to install
specs[R]

@return [Array<Specification>]

the spec whose workspace is being created

Public Class Methods

new(configuration, specs, podfile) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 36
def initialize(configuration, specs, podfile)
  @configuration = configuration
  @specs = specs
  @podfile = podfile
end

Public Instance Methods

install!() click to toggle source

Installs the {podfile} into the {install_directory}

@return [void]

# File lib/cocoapods/generate/installer.rb, line 53
def install!
  UI.title "Generating workspace in #{UI.path install_directory}" do
    clean! if configuration.clean?
    install_directory.mkpath

    UI.message 'Creating stub application' do
      create_app_project
    end

    UI.message 'Writing Podfile' do
      podfile.defined_in_file.open('w') { |f| f << podfile.to_yaml }
    end

    installer = nil
    UI.section 'Installing...' do
      configuration.pod_config.with_changes(installation_root: install_directory, podfile: podfile,
                                            lockfile: configuration.lockfile, sandbox: nil,
                                            sandbox_root: install_directory + 'Pods',
                                            podfile_path: podfile.defined_in_file,
                                            silent: !configuration.pod_config.verbose?, verbose: false,
                                            lockfile_path: nil) do
        installer = ::Pod::Installer.new(configuration.pod_config.sandbox, podfile, configuration.lockfile)
        installer.use_default_plugins = configuration.use_default_plugins
        installer.install!
      end
    end

    UI.section 'Performing post-installation steps' do
      should_perform_post_install = if installer.respond_to?(:generated_aggregate_targets) # CocoaPods 1.7.0
                                      !installer.generated_aggregate_targets.empty?
                                    else
                                      true
                                    end
      perform_post_install_steps(open_app_project, installer) if should_perform_post_install
    end

    print_post_install_message
  end
end
install_directory() click to toggle source

@return [Pathname]

The directory that pods will be installed into
# File lib/cocoapods/generate/installer.rb, line 45
def install_directory
  @install_directory ||= podfile.defined_in_file.dirname
end

Private Instance Methods

add_test_spec_schemes_to_app_scheme(installer, app_project) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 281
def add_test_spec_schemes_to_app_scheme(installer, app_project)
  spec_root_names = Set.new(specs.map { |s| s.root.name })

  test_native_targets =
    installer
    .target_installation_results
    .pod_target_installation_results
    .values
    .flatten(1)
    .select { |installation_result| spec_root_names.include?(installation_result.target.pod_name) }
    .flat_map(&:test_native_targets)
    .group_by(&:platform_name)

  workspace_path = install_directory + (configuration.project_name_for_specs(specs) + '.xcworkspace')
  Xcodeproj::Plist.write_to_path(
    { 'IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded' => false },
    workspace_path.join('xcshareddata').tap(&:mkpath).join('WorkspaceSettings.xcsettings')
  )

  test_native_targets.each do |platform_name, test_targets|
    app_scheme_path = Xcodeproj::XCScheme.shared_data_dir(app_project.path).join("App-#{Platform.string_name(platform_name)}.xcscheme")
    raise "Missing app scheme for #{platform_name}: #{app_scheme_path.inspect}" unless app_scheme_path.file?

    app_scheme = Xcodeproj::XCScheme.new(app_scheme_path)
    test_action = app_scheme.test_action
    existing_test_targets = test_action.testables.flat_map(&:buildable_references).map(&:target_name)

    test_targets.sort_by(&:name).each do |target|
      next if existing_test_targets.include?(target.name)

      testable = Xcodeproj::XCScheme::TestAction::TestableReference.new(target)
      testable.buildable_references.each do |buildable|
        buildable.xml_element.attributes['ReferencedContainer'] = "container:Pods/#{File.basename(target.project.path)}"
      end
      test_action.add_testable(testable)
    end

    app_scheme.save!
  end
end
clean!() click to toggle source

Removes the {install_directory}

@return [void]

# File lib/cocoapods/generate/installer.rb, line 99
def clean!
  UI.message 'Cleaning gen install directory' do
    FileUtils.rm_rf install_directory
  end
end
create_app_project() click to toggle source

Creates an app project that CocoaPods will integrate into

@return [Xcodeproj::Project]

# File lib/cocoapods/generate/installer.rb, line 122
def create_app_project
  app_project = open_app_project(recreate: !configuration.incremental_installation?)

  platforms_by_spec = Hash[specs.map do |spec|
    platforms = spec.available_platforms.flatten.reject do |platform|
      !configuration.platforms.nil? && !configuration.platforms.include?(platform.string_name.downcase)
    end
    [spec, platforms]
  end]

  if platforms_by_spec.values.all?(&:empty?)
    Pod::Command::Gen.help! Pod::StandardError.new "No available platforms for podspecs #{specs.map(&:name).to_sentence} match requested platforms: #{configuration.platforms}"
  end

  platforms_by_spec
    .flat_map do |spec, platforms|
      platforms.map do |platform|
        consumer = spec.consumer(platform)
        target_name = "App-#{Platform.string_name(consumer.platform_name)}"
        next if app_project.targets.map(&:name).include? target_name
        native_app_target = Pod::Generator::AppTargetHelper.add_app_target(app_project, consumer.platform_name,
                                                                           deployment_target(consumer), target_name)
        # Temporarily set Swift version to pass validator checks for pods which do not specify Swift version.
        # It will then be re-set again within #perform_post_install_steps.
        Pod::Generator::AppTargetHelper.add_swift_version(native_app_target, Pod::Validator::DEFAULT_SWIFT_VERSION)
        native_app_target
      end
    end
    .compact.uniq.tap do
      app_project.recreate_user_schemes do |scheme, target|
        installation_result = installation_result_from_target(target)
        next unless installation_result
        installation_result.test_native_targets.each do |test_native_target|
          scheme.add_test_target(test_native_target)
        end
      end
    end
    .each do |target|
      Xcodeproj::XCScheme.share_scheme(app_project.path.to_s, target.name) if target
    end
  app_project.save
  app_project
end
create_main_source_file(project, pod_targets, name) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 416
def create_main_source_file(project, pod_targets, name)
  source_file = project.path.dirname.+("#{name}/main.m")
  source_file.parent.mkpath

  import_statements = pod_targets.map do |pod_target|
    if pod_target.should_build? && pod_target.defines_module?
      "@import #{pod_target.product_module_name};"
    else
      header_name = "#{pod_target.product_module_name}/#{pod_target.product_module_name}.h"
      "#import <#{header_name}>" if pod_target.sandbox.public_headers.root.+(header_name).file?
    end
  end.compact

  source_file.open('w') do |f|
    f << import_statements.join("\n")
    f << "\n" unless import_statements.empty?
    f << "int main() { return 0; }\n"
  end

  source_file
end
deployment_target(consumer) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 166
def deployment_target(consumer)
  deployment_target = consumer.spec.deployment_target(consumer.platform_name)
  if consumer.platform_name == :ios && configuration.use_frameworks?
    minimum = Version.new('8.0')
    deployment_target = [Version.new(deployment_target), minimum].max.to_s
  end
  deployment_target
end
group_for_platform_name(project, platform_name, should_create = true) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 394
def group_for_platform_name(project, platform_name, should_create = true)
  project.main_group.find_subpath("App-#{platform_name}", should_create)
end
installation_result_from_target(target) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 267
def installation_result_from_target(target)
  return unless target.respond_to?(:symbol_type)
  library_product_types = %i[framework dynamic_library static_library]
  return unless library_product_types.include? target.symbol_type

  results_by_native_target[target]
end
make_ios_app_launchable(app_project, native_app_target) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 322
      def make_ios_app_launchable(app_project, native_app_target)
        platform_name = Platform.string_name(native_app_target.platform_name)
        generated_source_dir = install_directory.join("App-#{platform_name}").tap(&:mkpath)

        # Add `LaunchScreen.storyboard`
        launch_storyboard = generated_source_dir.join('LaunchScreen.storyboard')
        launch_storyboard.write <<-XML.strip_heredoc
              <?xml version="1.0" encoding="UTF-8" standalone="no"?>
              <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A277" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
                <dependencies>
                  <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
                  <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
                </dependencies>
                <scenes>
                  <!--View Controller-->
                  <scene sceneID="EHf-IW-A2E">
                    <objects>
                      <viewController id="01J-lp-oVM" sceneMemberID="viewController">
                        <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
                          <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                          <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                          <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        </view>
                      </viewController>
                      <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
                    </objects>
                    <point key="canvasLocation" x="53" y="375"/>
                  </scene>
                </scenes>
              </document>
            XML

        # Add & wire `Info.plist`
        info_plist_contents = {
          'CFBundleDevelopmentRegion' => '$(DEVELOPMENT_LANGUAGE)',
          'CFBundleExecutable' => '$(EXECUTABLE_NAME)',
          'CFBundleIdentifier' => '$(PRODUCT_BUNDLE_IDENTIFIER)',
          'CFBundleInfoDictionaryVersion' => '6.0',
          'CFBundleName' => '$(PRODUCT_NAME)',
          'CFBundlePackageType' => 'APPL',
          'CFBundleShortVersionString' => '1.0',
          'CFBundleVersion' => '1',
          'LSRequiresIPhoneOS' => true,
          'UILaunchStoryboardName' => 'LaunchScreen',
          'UIRequiredDeviceCapabilities' => [
            'armv7'
          ],
          'UISupportedInterfaceOrientations' => %w[
            UIInterfaceOrientationPortrait
            UIInterfaceOrientationLandscapeLeft
            UIInterfaceOrientationLandscapeRight
          ],
          'UISupportedInterfaceOrientations~ipad' => %w[
            UIInterfaceOrientationPortrait
            UIInterfaceOrientationPortraitUpsideDown
            UIInterfaceOrientationLandscapeLeft
            UIInterfaceOrientationLandscapeRight
          ]
        }
        info_plist_path = generated_source_dir.join('Info.plist')
        Xcodeproj::Plist.write_to_path(info_plist_contents, info_plist_path)

        native_app_target.build_configurations.each do |bc|
          bc.build_settings['INFOPLIST_FILE'] = "${SRCROOT}/App-#{platform_name}/Info.plist"
        end

        group = group_for_platform_name(app_project, platform_name)
        group.files.find { |f| f.display_name == 'Info.plist' } || group.new_file(info_plist_path)
        launch_storyboard_file_ref = group.files.find { |f| f.display_name == 'LaunchScreen.storyboard' } || group.new_file(launch_storyboard)
        native_app_target.resources_build_phase.add_file_reference(launch_storyboard_file_ref)
      end
open_app_project(recreate: false) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 105
def open_app_project(recreate: false)
  app_project_path = install_directory.join("#{configuration.project_name_for_specs(specs)}.xcodeproj")
  if !recreate && app_project_path.exist?
    Xcodeproj::Project.open(app_project_path)
  else
    version_key = XCODE_VERSION_TO_OBJECT_VERSION.keys.find do |k|
      configuration.xcode_version >= Pod::Version.new(k)
    end || DEFAULT_XCODE_VERSION
    object_version = XCODE_VERSION_TO_OBJECT_VERSION[version_key]
    Xcodeproj::Project.new(app_project_path, false, object_version)
  end
end
perform_post_install_steps(app_project, installer) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 175
def perform_post_install_steps(app_project, installer)
  app_project.native_targets.each do |native_app_target|
    remove_script_phase_from_target(native_app_target, 'Check Pods Manifest.lock')

    spec_names = specs.map(&:name)
    pod_targets = installer.pod_targets.select do |pt|
      pt.platform.name == native_app_target.platform_name && spec_names.include?(pt.pod_name)
    end

    native_app_target.source_build_phase.clear
    native_app_target.resources_build_phase.clear

    if (app_host_source_dir = configuration.app_host_source_dir)
      relative_app_host_source_dir = app_host_source_dir.relative_path_from(installer.sandbox.root)
      groups = {}

      app_host_source_dir.find do |file|
        relative_path = file.relative_path_from(app_host_source_dir)

        if file.directory?
          groups[relative_path] =
            if (base_group = groups[relative_path.dirname])
              basename = relative_path.basename
              base_group.new_group(basename.to_s, basename)
            else
              app_project.new_group(native_app_target.name, relative_app_host_source_dir)
            end

          next
        elsif file.to_s.end_with?('-Bridging-Header.h')
          native_app_target.build_configurations.each do |bc|
            if (old_bridging_header = bc.build_settings['SWIFT_OBJC_BRIDGING_HEADER'])
              raise Informative, "Conflicting Swift ObjC bridging headers specified, got #{old_bridging_header} and #{relative_path}. Only one `-Bridging-Header.h` file may be specified in the app host source dir."
            end

            bc.build_settings['SWIFT_OBJC_BRIDGING_HEADER'] = relative_path.to_s
          end
        end

        group = groups[relative_path.dirname]
        source_file_ref = group.new_file(file.basename)
        native_app_target.add_file_references([source_file_ref])
      end
    else
      platform_name = Platform.string_name(native_app_target.platform_name)
      group = group_for_platform_name(app_project, platform_name)
      main_file_ref = group.files.find { |f| f.display_name == 'main.m' }
      if main_file_ref.nil?
        source_file = create_main_source_file(app_project, pod_targets, native_app_target.name)
        group = app_project[group.name] || app_project.new_group(group.name, group.name)
        source_file_ref = group.new_file(source_file)
        native_app_target.add_file_references([source_file_ref])
      else
        native_app_target.add_file_references([main_file_ref])
      end
    end

    # Set `PRODUCT_BUNDLE_IDENTIFIER`
    native_app_target.build_configurations.each do |bc|
      bc.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'org.cocoapods-generate.${PRODUCT_NAME:rfc1034identifier}'
    end

    case native_app_target.platform_name
    when :ios
      make_ios_app_launchable(app_project, native_app_target)
    end

    swift_version = pod_targets.map { |pt| Pod::Version.new(pt.swift_version) }.max.to_s

    Pod::Generator::AppTargetHelper.add_swift_version(native_app_target, swift_version) unless swift_version.blank?
    if installer.pod_targets.any? { |pt| pt.spec_consumers.any? { |c| c.frameworks.include?('XCTest') } }
      Pod::Generator::AppTargetHelper.add_xctest_search_paths(native_app_target)
    end

    pod_targets.each do |pod_target|
      result = installer.target_installation_results.pod_target_installation_results[pod_target.name]
      next unless result
      share_scheme(result.native_target.project, pod_target.label)
      pod_target.test_specs.each do |test_spec|
        share_scheme(result.native_target.project, pod_target.test_target_label(test_spec))
      end
      pod_target.app_specs.each do |app_spec|
        share_scheme(result.native_target.project, pod_target.app_target_label(app_spec))
      end
    end

    add_test_spec_schemes_to_app_scheme(installer, app_project)
  end

  app_project.save
end
print_post_install_message() click to toggle source
# File lib/cocoapods/generate/installer.rb, line 398
def print_post_install_message
  workspace_path = install_directory.join(podfile.workspace_path)

  if configuration.auto_open?
    configuration.pod_config.with_changes(verbose: true) do
      Executable.execute_command 'open', [workspace_path]
    end
  else
    UI.info "Open #{UI.path workspace_path} to work on it!"
  end
end
remove_script_phase_from_target(native_target, script_phase_name) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 275
def remove_script_phase_from_target(native_target, script_phase_name)
  script_phase = native_target.shell_script_build_phases.find { |bp| bp.name && bp.name.end_with?(script_phase_name) }
  return unless script_phase.present?
  native_target.build_phases.delete(script_phase)
end
share_scheme(project, scheme_name) click to toggle source
# File lib/cocoapods/generate/installer.rb, line 410
def share_scheme(project, scheme_name)
  scheme = Xcodeproj::XCScheme.user_data_dir(project.path) + "#{scheme_name}.xcscheme"
  return unless File.exist?(scheme)
  Xcodeproj::XCScheme.share_scheme(project.path, scheme_name)
end