Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate ODRs for app specs. #10845

Merged
merged 1 commit into from Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changelog entry was in the wrong spot.

[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'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pod install on this example warning fix.

# 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
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this became a "helper" class method as the others. Helps do the job easily without repeating stuff in both target_integrator and pod_target_integrator.

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
dnkoutso marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't know the structure of pod_target.test_app_hosts_by_spec[fa.spec] but should this check be pod_target.test_app_hosts_by_spec[fa.spec]&.include?(app_spec) rather than only checking first?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns a tuple:

    # @return [Hash{Specification => (Specification,PodTarget)}] tuples of app specs and pod targets by test spec.
    #
    attr_accessor :test_app_hosts_by_spec

I am interested only on the first part to match against the spec.

We could potentially expose a method in pod_target but this property is public for now.

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDE warning fix

#
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?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDE warning fix

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