Skip to content

Commit

Permalink
Merge pull request CocoaPods#10845 from dnkoutso/odr_integrate_app_specs
Browse files Browse the repository at this point in the history
Integrate ODRs for app specs.
  • Loading branch information
dnkoutso committed Aug 6, 2021
2 parents 4c77243 + 9d28c49 commit 9cebcde
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 73 deletions.
11 changes: 6 additions & 5 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,12 @@ To install release candidates run `[sudo] gem install cocoapods --pre`

##### Enhancements

* Add support for integrating on demand resources.
[Dimitris Koutsogiorgas](https://github.com/dnkoutso)
[JunyiXie](https://github.com/JunyiXie)
[#9606](https://github.com/CocoaPods/CocoaPods/issues/9606)
[#10845](https://github.com/CocoaPods/CocoaPods/pull/10845)

* Integrate `project_header_files` specified by specs.
[Dimitris Koutsogiorgas](https://github.com/dnkoutso)
[#9820](https://github.com/CocoaPods/CocoaPods/issues/9820)
Expand Down Expand Up @@ -277,11 +283,6 @@ To install release candidates run `[sudo] gem install cocoapods --pre`

##### Enhancements

* Add support for integrating on demand resources.
[Dimitris Koutsogiorgas](https://github.com/dnkoutso)
[JunyiXie](https://github.com/JunyiXie)
[#9606](https://github.com/CocoaPods/CocoaPods/issues/9606)

* Add the App Clip product symbol to the list of products that need embedding.
[Igor Makarov](https://github.com/igor-makarov)
[#9882](https://github.com/CocoaPods/CocoaPods/pull/9882)
Expand Down
3 changes: 2 additions & 1 deletion examples/OnDemandResources Example/Podfile
Expand Up @@ -2,9 +2,10 @@
# platform :ios, '9.0'

target 'OnDemandResourcesDemo' do
platform :ios, '13.2'
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'TestLibrary', :path => './TestLibrary'
pod 'TestLibrary', :path => './TestLibrary', :appspecs => ['App1', 'App2'], :testspecs => ['Tests']

# Pods for OnDemandResourcesDemo

Expand Down
@@ -0,0 +1,20 @@
import UIKit

class ViewController: UIViewController {
override func viewDidLoad() {
view.backgroundColor = .green
}
}

@UIApplicationMain
class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController(rootViewController: ViewController())
window?.makeKeyAndVisible()

return true
}
}
@@ -0,0 +1,20 @@
import UIKit

class ViewController: UIViewController {
override func viewDidLoad() {
view.backgroundColor = .green
}
}

@UIApplicationMain
class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController(rootViewController: ViewController())
window?.makeKeyAndVisible()

return true
}
}
32 changes: 30 additions & 2 deletions examples/OnDemandResources Example/TestLibrary/TestLibrary.podspec
Expand Up @@ -28,10 +28,12 @@ TODO: Add long description of the pod here.
s.source = { :git => 'https://github.com/lizhuoli1126@126.com/TestLibrary.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

s.ios.deployment_target = '8.0'
s.ios.deployment_target = '9.0'

s.swift_version = '4'

s.source_files = 'TestLibrary/Classes/**/*'

s.on_demand_resources = {
't1' => ['on_demand_bundle1/*'],
't2' => ['on_demand_bundle2/*']
Expand All @@ -41,5 +43,31 @@ TODO: Add long description of the pod here.
'DEFINES_MODULE' => 'YES'
}

s.app_spec 'App1' do |app_spec|
app_spec.source_files = 'App1/Classes/**/*'

app_spec.on_demand_resources = {
'a1' => ['App1/app1_on_demand_bundle1/*']
}
end

s.app_spec 'App2' do |app_spec|
app_spec.source_files = 'App2/Classes/**/*'

app_spec.on_demand_resources = {
'a2' => ['App2/app2_on_demand_bundle1/*']
}
end

s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'Tests/Classes/**/*'

test_spec.app_host_name = 'TestLibrary/App1'
test_spec.requires_app_host = true
test_spec.dependency 'TestLibrary/App1'

test_spec.on_demand_resources = {
'test1' => ['Tests/test_on_demand_bundle/*']
}
end
end
Empty file.
148 changes: 88 additions & 60 deletions lib/cocoapods/installer/user_project_integrator/target_integrator.rb
Expand Up @@ -433,6 +433,85 @@ def embed_frameworks_output_paths(framework_paths, xcframeworks)
end
paths + xcframework_paths
end

# Updates a projects native targets to include on demand resources specified by the supplied parameters.
# Note that currently, only app level targets are allowed to include on demand resources.
#
# @param [Sandbox] sandbox
# The sandbox to use for calculating ODR file references.
#
# @param [Xcodeproj::Project] project
# The project to update known asset tags as well as add the ODR group.
#
# @param [Xcodeproj::PBXNativeTarget, Array<Xcodeproj::PBXNativeTarget>] native_targets
# The native targets to integrate on demand resources into.
#
# @param [Sandbox::FileAccessor, Array<Sandbox::FileAccessor>] file_accessors
# The file accessors that that provide the ODRs to integrate.
#
# @param [Xcodeproj::PBXGroup] parent_odr_group
# The group to use as the parent to add ODR file references into.
#
# @param [String] target_odr_group_name
# The name to use for the group created that contains the ODR file references.
#
# @return [void]
#
def add_on_demand_resources(sandbox, project, native_targets, file_accessors, parent_odr_group,
target_odr_group_name)
asset_tags_added = Set.new
file_accessors = Array(file_accessors)
native_targets = Array(native_targets)

# Target no longer provides ODR references so remove everything related to this target.
if file_accessors.all? { |fa| fa.on_demand_resources.empty? }
old_target_odr_group = parent_odr_group[target_odr_group_name]
old_odr_file_refs = old_target_odr_group&.recursive_children_groups&.each_with_object({}) do |group, hash|
hash[group.name] = group.files
end || {}
native_targets.each do |user_target|
user_target.remove_on_demand_resources(old_odr_file_refs)
end
old_target_odr_group&.remove_from_project
return
end

target_odr_group = parent_odr_group[target_odr_group_name] || parent_odr_group.new_group(target_odr_group_name)
current_file_refs = target_odr_group.recursive_children_groups.flat_map(&:files)

added_file_refs = file_accessors.flat_map do |file_accessor|
target_odr_files_refs = Hash[file_accessor.on_demand_resources.map do |tag, resources|
tag_group = target_odr_group[tag] || target_odr_group.new_group(tag)
asset_tags_added << tag
resources_file_refs = resources.map do |resource|
odr_resource_file_ref = Pathname.new(resource).relative_path_from(sandbox.root)
tag_group.find_file_by_path(odr_resource_file_ref.to_s) || tag_group.new_file(odr_resource_file_ref)
end
[tag, resources_file_refs]
end]
native_targets.each do |user_target|
user_target.add_on_demand_resources(target_odr_files_refs)
end
target_odr_files_refs.values.flatten
end

# if the target ODR file references were updated, make sure we remove the ones that are no longer present
# for the target.
remaining_refs = current_file_refs - added_file_refs
remaining_refs.each do |ref|
native_targets.each do |user_target|
user_target.resources_build_phase.remove_file_reference(ref)
end
ref.remove_from_project
end
target_odr_group.recursive_children_groups.each { |g| g.remove_from_project if g.empty? }

unless asset_tags_added.empty?
attributes = project.root_object.attributes
attributes['KnownAssetTags'] = (attributes['KnownAssetTags'] ||= []) | asset_tags_added.to_a
project.root_object.attributes = attributes
end
end
end

# Integrates the user project targets. Only the targets that do **not**
Expand Down Expand Up @@ -632,68 +711,17 @@ def remove_obsolete_script_phases(removed_phase_names = REMOVED_SCRIPT_PHASE_NAM
end
end

# Updates all user targets to include on demand resources specified by libraries. Note that currently,
# only app level targets are allowed to include on demand resources.
#
# @return [void]
#
def add_on_demand_resources
user_project = target.user_project

asset_tags_added = target.pod_targets.each_with_object(Set.new) do |pod_target, asset_tags|
target_odr_group_name = "#{pod_target.label}-OnDemandResources"
target.pod_targets.each do |pod_target|
# When integrating with the user's project we are only interested in integrating ODRs from library specs
# and not test specs or app specs.
library_file_accessors = pod_target.file_accessors.select { |fa| fa.spec.library_specification? }

# Target no longer provides ODR references so remove everything related to this target.
if library_file_accessors.all? { |fa| fa.on_demand_resources.empty? }
old_target_odr_group = user_project.main_group.find_subpath("Pods/#{target_odr_group_name}")
old_odr_file_refs = old_target_odr_group&.recursive_children_groups&.each_with_object({}) do |group, hash|
hash[group.name] = group.files
end || {}
target.user_targets.each do |user_target|
user_target.remove_on_demand_resources(old_odr_file_refs)
end
old_target_odr_group&.remove_from_project
next
end

target_odr_group = user_project['Pods'][target_odr_group_name] || user_project['Pods'].new_group(target_odr_group_name)
current_file_refs = target_odr_group.recursive_children_groups.flat_map(&:files)

added_file_refs = library_file_accessors.flat_map do |file_accessor|
target_odr_files_refs = Hash[file_accessor.on_demand_resources.map do |tag, resources|
tag_group = target_odr_group[tag] || target_odr_group.new_group(tag)
asset_tags << tag
resources_file_refs = resources.map do |resource|
odr_resource_file_ref = Pathname.new(resource).relative_path_from(target.sandbox.root)
tag_group.find_file_by_path(odr_resource_file_ref.to_s) || tag_group.new_file(odr_resource_file_ref)
end
[tag, resources_file_refs]
end]
target.user_targets.each do |user_target|
user_target.add_on_demand_resources(target_odr_files_refs)
end
target_odr_files_refs.values.flatten
end

# if the target ODR file references were updated, make sure we remove the ones that are no longer present
# for the target.
remaining_refs = current_file_refs - added_file_refs
unless remaining_refs.empty?
remaining_refs.each do |ref|
target.user_targets.each do |user_target|
user_target.resources_build_phase.remove_file_reference(ref)
end
ref.remove_from_project
end
target_odr_group.recursive_children_groups.each { |g| g.remove_from_project if g.empty? }
end
end

unless asset_tags_added.empty?
attributes = user_project.root_object.attributes
attributes['KnownAssetTags'] = (attributes['KnownAssetTags'] ||= []) | asset_tags_added.to_a
target.user_project.root_object.attributes = attributes
target_odr_group_name = "#{pod_target.label}-OnDemandResources"
# The 'Pods' group would always be there for production code however for tests its sometimes not added.
# This ensures its always present and makes it easier for existing and new tests.
parent_odr_group = target.user_project.main_group['Pods'] || target.user_project.new_group('Pods')
TargetIntegrator.add_on_demand_resources(target.sandbox, target.user_project, target.user_targets,
library_file_accessors, parent_odr_group, target_odr_group_name)
end
end

Expand Down
Expand Up @@ -34,6 +34,7 @@ def integrate!
target_installation_result.non_library_specs_by_native_target.each do |native_target, spec|
add_embed_frameworks_script_phase(native_target, spec)
add_copy_resources_script_phase(native_target, spec)
add_on_demand_resources(native_target, spec) if spec.app_specification?
UserProjectIntegrator::TargetIntegrator.create_or_update_user_script_phases(script_phases_for_specs(spec), native_target)
end
add_copy_dsyms_script_phase(target_installation_result.native_target)
Expand Down Expand Up @@ -205,7 +206,7 @@ def add_copy_xcframeworks_script_phase(native_target)
# vendored dSYMs.
#
# @param [PBXNativeTarget] native_target
# the native target for which to add the the copy dSYM files build phase.
# the native target for which to add the copy dSYM files build phase.
#
# @return [void]
#
Expand Down Expand Up @@ -248,6 +249,41 @@ def add_copy_dsyms_script_phase(native_target)
UserProjectIntegrator::TargetIntegrator.set_input_output_paths(phase, input_paths_by_config, output_paths_by_config)
end

# Adds the ODRs that are related to this app spec. This includes the app spec dependencies as well as the ODRs
# coming from the app spec itself.
#
# @param [Xcodeproj::PBXNativeTarget] native_target
# the native target for which to add the ODR file references into.
#
# @param [Specification] app_spec
# the app spec to integrate ODRs for.
#
# @return [void]
#
def add_on_demand_resources(native_target, app_spec)
dependent_targets = target.dependent_targets_for_app_spec(app_spec)
parent_odr_group = native_target.project.group_for_spec(app_spec.name)

# Add ODRs of the app spec dependencies first.
dependent_targets.each do |pod_target|
file_accessors = pod_target.file_accessors.select do |fa|
fa.spec.library_specification? ||
fa.spec.test_specification? && pod_target.test_app_hosts_by_spec[fa.spec]&.first == app_spec
end
target_odr_group_name = "#{pod_target.label}-OnDemandResources"
UserProjectIntegrator::TargetIntegrator.add_on_demand_resources(target.sandbox, native_target.project,
native_target, file_accessors,
parent_odr_group, target_odr_group_name)
end

# Now add the ODRs of our own app spec declaration.
file_accessor = target.file_accessors.find { |fa| fa.spec == app_spec }
target_odr_group_name = "#{target.subspec_label(app_spec)}-OnDemandResources"
UserProjectIntegrator::TargetIntegrator.add_on_demand_resources(target.sandbox, native_target.project,
native_target, file_accessor,
parent_odr_group, target_odr_group_name)
end

# @return [String] the message that should be displayed for the target
# integration.
#
Expand Down
Expand Up @@ -79,12 +79,12 @@ def initialize(target, native_target, resource_bundle_targets = [], test_native_
# @param [Specification] spec
# The specification to base from in order to find the native target.
#
# @return [PBXNativeTarget] the native target to use or `nil` if none is found.
# @return [PBXNativeTarget, Nil] the native target to use or `nil` if none is found.
#
def native_target_for_spec(spec)
return native_target if spec.library_specification?
return test_native_target_from_spec(spec) if spec.test_specification?
return app_native_target_from_spec(spec) if spec.app_specification?
app_native_target_from_spec(spec) if spec.app_specification?
end

# @return [Hash{PBXNativeTarget => Specification}] a hash where the keys are the test native targets and the value
Expand Down
4 changes: 2 additions & 2 deletions lib/cocoapods/target/pod_target.rb
Expand Up @@ -311,7 +311,7 @@ def test_spec_consumers
test_specs.map { |test_spec| test_spec.consumer(platform) }
end

# @return [Array<Specification::Consumer>] the test specification consumers for
# @return [Array<Specification::Consumer>] the app specification consumers for
# the target.
#
def app_spec_consumers
Expand Down Expand Up @@ -394,7 +394,7 @@ def contains_test_specifications?
!test_specs.empty?
end

# @return [Boolean] Whether the target has any tests specifications.
# @return [Boolean] Whether the target has any app specifications.
#
def contains_app_specifications?
!app_specs.empty?
Expand Down

0 comments on commit 9cebcde

Please sign in to comment.