From 9d28c498879a3d87dae8c1c12ebe2642573bf303 Mon Sep 17 00:00:00 2001 From: Dimitris Koutsogiorgas Date: Wed, 4 Aug 2021 01:05:03 +0300 Subject: [PATCH] Integrate ODRs for app specs. --- CHANGELOG.md | 11 +- examples/OnDemandResources Example/Podfile | 3 +- .../App1/Classes/AppDelegate.swift | 20 +++ .../app1_on_demand_bundle1/resource} | 0 .../App2/Classes/AppDelegate.swift | 20 +++ .../app2_on_demand_bundle1/resource} | 0 .../TestLibrary/TestLibrary.podspec | 32 +++- .../Classes/TestLibraryTests.m} | 0 .../test_on_demand_bundle/on1_sub1/resource | 0 .../Tests/test_on_demand_bundle/resource | 0 .../target_integrator.rb | 148 +++++++++++------- .../pod_target_integrator.rb | 38 ++++- .../target_installation_result.rb | 4 +- lib/cocoapods/target/pod_target.rb | 4 +- 14 files changed, 207 insertions(+), 73 deletions(-) create mode 100644 examples/OnDemandResources Example/TestLibrary/App1/Classes/AppDelegate.swift rename examples/OnDemandResources Example/TestLibrary/{OnDemandResources/TestFile1 => App1/app1_on_demand_bundle1/resource} (100%) create mode 100644 examples/OnDemandResources Example/TestLibrary/App2/Classes/AppDelegate.swift rename examples/OnDemandResources Example/TestLibrary/{TestLibrary/Assets/.gitkeep => App2/app2_on_demand_bundle1/resource} (100%) rename examples/OnDemandResources Example/TestLibrary/{TestLibrary/Classes/.gitkeep => Tests/Classes/TestLibraryTests.m} (100%) create mode 100644 examples/OnDemandResources Example/TestLibrary/Tests/test_on_demand_bundle/on1_sub1/resource create mode 100644 examples/OnDemandResources Example/TestLibrary/Tests/test_on_demand_bundle/resource diff --git a/CHANGELOG.md b/CHANGELOG.md index a14c5294ce..4a5e725584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) @@ -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) diff --git a/examples/OnDemandResources Example/Podfile b/examples/OnDemandResources Example/Podfile index 5eb7efdbf8..7de1493fc9 100644 --- a/examples/OnDemandResources Example/Podfile +++ b/examples/OnDemandResources Example/Podfile @@ -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 diff --git a/examples/OnDemandResources Example/TestLibrary/App1/Classes/AppDelegate.swift b/examples/OnDemandResources Example/TestLibrary/App1/Classes/AppDelegate.swift new file mode 100644 index 0000000000..5769211821 --- /dev/null +++ b/examples/OnDemandResources Example/TestLibrary/App1/Classes/AppDelegate.swift @@ -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 + } +} diff --git a/examples/OnDemandResources Example/TestLibrary/OnDemandResources/TestFile1 b/examples/OnDemandResources Example/TestLibrary/App1/app1_on_demand_bundle1/resource similarity index 100% rename from examples/OnDemandResources Example/TestLibrary/OnDemandResources/TestFile1 rename to examples/OnDemandResources Example/TestLibrary/App1/app1_on_demand_bundle1/resource diff --git a/examples/OnDemandResources Example/TestLibrary/App2/Classes/AppDelegate.swift b/examples/OnDemandResources Example/TestLibrary/App2/Classes/AppDelegate.swift new file mode 100644 index 0000000000..5769211821 --- /dev/null +++ b/examples/OnDemandResources Example/TestLibrary/App2/Classes/AppDelegate.swift @@ -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 + } +} diff --git a/examples/OnDemandResources Example/TestLibrary/TestLibrary/Assets/.gitkeep b/examples/OnDemandResources Example/TestLibrary/App2/app2_on_demand_bundle1/resource similarity index 100% rename from examples/OnDemandResources Example/TestLibrary/TestLibrary/Assets/.gitkeep rename to examples/OnDemandResources Example/TestLibrary/App2/app2_on_demand_bundle1/resource diff --git a/examples/OnDemandResources Example/TestLibrary/TestLibrary.podspec b/examples/OnDemandResources Example/TestLibrary/TestLibrary.podspec index eb713e1d2b..db8f8de06f 100644 --- a/examples/OnDemandResources Example/TestLibrary/TestLibrary.podspec +++ b/examples/OnDemandResources Example/TestLibrary/TestLibrary.podspec @@ -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/' - 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/*'] @@ -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 diff --git a/examples/OnDemandResources Example/TestLibrary/TestLibrary/Classes/.gitkeep b/examples/OnDemandResources Example/TestLibrary/Tests/Classes/TestLibraryTests.m similarity index 100% rename from examples/OnDemandResources Example/TestLibrary/TestLibrary/Classes/.gitkeep rename to examples/OnDemandResources Example/TestLibrary/Tests/Classes/TestLibraryTests.m diff --git a/examples/OnDemandResources Example/TestLibrary/Tests/test_on_demand_bundle/on1_sub1/resource b/examples/OnDemandResources Example/TestLibrary/Tests/test_on_demand_bundle/on1_sub1/resource new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/OnDemandResources Example/TestLibrary/Tests/test_on_demand_bundle/resource b/examples/OnDemandResources Example/TestLibrary/Tests/test_on_demand_bundle/resource new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/cocoapods/installer/user_project_integrator/target_integrator.rb b/lib/cocoapods/installer/user_project_integrator/target_integrator.rb index c323b1d7fe..02c496efea 100644 --- a/lib/cocoapods/installer/user_project_integrator/target_integrator.rb +++ b/lib/cocoapods/installer/user_project_integrator/target_integrator.rb @@ -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] native_targets + # The native targets to integrate on demand resources into. + # + # @param [Sandbox::FileAccessor, Array] 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** @@ -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 diff --git a/lib/cocoapods/installer/xcode/pods_project_generator/pod_target_integrator.rb b/lib/cocoapods/installer/xcode/pods_project_generator/pod_target_integrator.rb index 6df0d66ef8..a9af13d52a 100644 --- a/lib/cocoapods/installer/xcode/pods_project_generator/pod_target_integrator.rb +++ b/lib/cocoapods/installer/xcode/pods_project_generator/pod_target_integrator.rb @@ -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) @@ -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] # @@ -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. # diff --git a/lib/cocoapods/installer/xcode/pods_project_generator/target_installation_result.rb b/lib/cocoapods/installer/xcode/pods_project_generator/target_installation_result.rb index 2a072b7e05..3e1d55e632 100644 --- a/lib/cocoapods/installer/xcode/pods_project_generator/target_installation_result.rb +++ b/lib/cocoapods/installer/xcode/pods_project_generator/target_installation_result.rb @@ -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 diff --git a/lib/cocoapods/target/pod_target.rb b/lib/cocoapods/target/pod_target.rb index 46f0e6f9c9..a8957a2e3c 100644 --- a/lib/cocoapods/target/pod_target.rb +++ b/lib/cocoapods/target/pod_target.rb @@ -311,7 +311,7 @@ def test_spec_consumers test_specs.map { |test_spec| test_spec.consumer(platform) } end - # @return [Array] the test specification consumers for + # @return [Array] the app specification consumers for # the target. # def app_spec_consumers @@ -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?