Skip to content

Automate copy-frameworks command? #2605

@dimazen

Description

@dimazen

Hello Carthage team!

As you know in order to get things done we need to invoke copy-frameworks at the Build Phase. This also involves specifying a list of input files for each linked framework. This seems to be a good subject for an automation. Currently it might be a little annoying to keep this list in sync and to manually fill it in. This is the main motivation.

From my attempt to make a draft of such functionality here is what I found:
Initially we need to grab list of the linked frameworks that was built by Carthage. Turns out that they are stored in the Frameworks Build Phase. Unfortunately built-in tool xcodebuild doesn't allow us to read such information, therefore we used to read contents of the .xcodeproj itself. There are well-known tools such as gem xcodeproj and its Swift counterpart xcodeproj.

First of all we're reading contents of the Frameworks Build Phase. The rest of the process is very simple: select only those frameworks that are located in Carthage/Build but not in the Static subfolder.
Then we simply iterating over found frameworks and filling in env variables like SCRIPT_INPUT_FILE_ and so on.

Below you can find working draft in ruby:

#!/usr/bin/env ruby

require 'xcodeproj'

# We need to find linked frameworks that are located in the `Carthage/Build/*` folder
# but also doesn't appear to be static.
# Focus on the linked frameworks prevents us from accidental copying of the
# frameworks that belong to a different target (e.g. UnitTests).
def find_linked_frameworks_refs
  # These env variables passed by Xcode
  # during invocation of the Run Script Build Phase
  # Therefore running script on its own will have no effect.
  project_file_path = ENV["PROJECT_FILE_PATH"]
  target_name = ENV["TARGET_NAME"]

  return [] if project_file_path.nil? || target_name.nil?

  project = Xcodeproj::Project.open(project_file_path)
  target = project.targets.detect { |target| target.name == target_name }

  return [] if target.nil?

  refs = target.frameworks_build_phases.files_references

  # Selecting only Dynamic Framworks built by Carthage. 
  # Static frameworks also appears in this list but we don't need to process them.
  regexp = %r{^Carthage\/Build\/((?!\/Static\/).)*$}
  refs.select do |ref|
    regexp.match(ref.path) != nil
  end
end

def export_io_vars_for_refs(refs)
  # The same: env vars exported by Xcode. Manual invocation will have no effect.
  srcroot = ENV["SRCROOT"]
  build_products_directory = ENV["BUILT_PRODUCTS_DIR"]
  frameworks_folder_path = ENV["FRAMEWORKS_FOLDER_PATH"]

  return if srcroot.nil? || build_products_directory.nil? || frameworks_folder_path.nil?

  refs.each_with_index do |ref, index|
    # Ref has a relative path. We assume that Carthage folder located in the $SRCROOT.
    input_path = File.join(srcroot, ref.path)
    # Exporting variables for `carthage copy-frameworks`.
    ENV["SCRIPT_INPUT_FILE_#{index}"] = input_path

    # Specify output files to speed up execution.
    # My concern at this point is that I'm not sure whether specifying these
    # vars within script itself actually have any impact on Xcode.
    # It might be the case when Xcode evaluates equality of the input/output
    # files prior to run of the script.
    output_path = File.join(build_products_directory, frameworks_folder_path, File.basename(ref.path))
    ENV["SCRIPT_OUTPUT_FILE_#{index}"] = output_path
  end

  ENV["SCRIPT_INPUT_FILE_COUNT"] = refs.count.to_s
  ENV["SCRIPT_OUTPUT_FILE_COUNT"] = refs.count.to_s
end

refs = find_linked_frameworks_refs
if refs.count > 0 then
  export_io_vars_for_refs(refs)
  exec "/usr/local/bin/carthage copy-frameworks"
end

Simply put it to the new build phase and invoke as follow:

ruby ${SRCROOT}/copy-frameworks.rb

Important: script requires Xcodeproj gem. Therefore is case you're managing ruby via rbenv, please use following script:

export PATH=~/.rbenv/shims:$PATH
ruby ${SRCROOT}/copy-frameworks.rb

The only downside at this point is that this automation would require from Carthage to add one more dependency (ruby gem or swift counterpart).

UPD:
Alternatives considered:
Simply copy contents of the Carthage/Build/... to the destination.
Pros: removes extra dependency
Cons: copies frameworks from other targets (if any) to the destination target.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions