###### CONSISTENCY BETWEEN MACOS AND IOS ##### | |
# | |
# In order to use the same PodFile with MacOS, we need to unlink the libraries that do not support Catalyst, filter | |
# files in native targets build phases, filter dependencies and make sure the unsupported frameworks along with their | |
# their bundle resources are not included in the final archive. For that, we use `platform_filter` to specify 'ios' and | |
# 'OTHER_LDFLAGS[sdk=iphone*]' to link those libraries for iPhone and iPad. Besides, we modify "*frameworks.sh" and | |
# "*resrouces.sh" to skip installation for architecture x86_64. | |
# | |
# *Notice*: 'sdk=iphone*' excludes macOS, even though Catalyst is compiled with iOS SDK. | |
# | |
# ADDING A NEW LIBRARY | |
# | |
# Pass the name of the new library to the script | |
# | |
###### RESOURCES ####### | |
# | |
# https://www.bitbuildr.com/tech-blog/mac-catalyst-porting-an-app-using-crashlytics-firebase - Article that inspired this script | |
# https://github.com/CocoaPods/Xcodeproj - Xcode configuration using ruby. This Framework is already included on cocoapods environment | |
# https://www.rubydoc.info/gems/xcodeproj/Xcodeproj/Project/Object/AbstractTarget Wiki for Xcodeproj | |
# | |
include Xcodeproj::Project::Object | |
include Pod | |
$verbose = false | |
def loggs string | |
if $verbose | |
puts string | |
end | |
return | |
end | |
# EXTENSIONS | |
class String | |
def filter_lines | |
lines = [] | |
each_line do |line| | |
if yield line | |
lines = lines + [line] | |
end | |
end | |
return lines | |
end | |
end | |
class PBXNativeTarget | |
###### STEP 4 ###### | |
# In "Pods-" targets, modify "*frameworks.sh" to not install unsupported frameworks for platform architectures | |
def uninstall_frameworks frameworks, platform, configurations | |
uninstall frameworks, "#{name}-frameworks.sh", platform.architectures, configurations | |
end | |
###### STEP 5 ###### | |
# In "Pods-" targets, modify "*resources.sh" to not install unsupported frameworks for platform architectures | |
def uninstall_resources resources, platform, configurations | |
uninstall resources, "#{name}-resources.sh", platform.architectures, configurations | |
end | |
def support_files_folder | |
build_configurations.filter do |config| not config.base_configuration_reference.nil? end.first.base_configuration_reference.real_path.parent | |
end | |
@private | |
def uninstall keys, file_name, architectures, configurations=nil | |
configurations = configurations.nil? ? build_configurations.map { |b| b.name } : configurations | |
keys = keys.to_set.to_a | |
loggs "\t\t\tUninstalling for configurations: #{configurations}" | |
if support_files_folder.nil? | |
loggs "\t\t\tNothing to uninstall" | |
return | |
end | |
script_path = support_files_folder.join file_name | |
if !script_path.exist? | |
loggs "\t\t\tNothing to uninstall" | |
return | |
end | |
script = File.read(script_path) | |
snippets = script.scan(/if \[\[ \"\$CONFIGURATION\" [\S\s]*?(?=fi\n)fi/) | |
condition = architectures.map do |arch| "[ \"$ARCHS\" != \"#{arch}\" ]" end.reduce("") do |total, piece| total.empty? ? piece : total + " || " + piece end | |
changed = false | |
snippets.filter do |snippet| | |
configurations.map do |string| snippet.include? string end.reduce(false) do |total, condition| total = total || condition end | |
end.each do |snippet| | |
new_snippet = snippet.clone | |
keys.each do |key| | |
lines_to_replace = snippet.filter_lines do |line| line.include? "#{key}" end.to_set.to_a | |
unless lines_to_replace.empty? | |
changed = true | |
lines_to_replace.each do |line| | |
new_snippet.gsub! line, "\tif #{condition}; then \n\t#{line}\tfi\n" | |
end | |
end | |
end | |
script.gsub! snippet, new_snippet | |
end | |
if changed | |
File.open(script_path, "w") { |file| file << script } | |
end | |
loggs "\t\t\t#{changed ? "Succeded" : "Nothing to uninstall"}" | |
end | |
###### STEP 1 ###### | |
# In native target's build phases, add platform filter to: | |
# - Resources | |
# - Compile Sources | |
# - Frameworks | |
# - Headers | |
def add_platform_filter_to_build_phases platform | |
loggs "\t\t- Filtering resources" | |
resources_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end | |
loggs "\t\t- Filtering compile sources" | |
source_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end | |
loggs "\t\t- Filtering frameworks" | |
frameworks_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end | |
loggs "\t\t- Filtering headers" | |
headers_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end | |
end | |
end | |
class PodTarget | |
def module_name | |
string = name.clone.gsub! /-iOS[0-9]+(\.[0-9])+$/, '' | |
return string.nil? ? name : string | |
end | |
def resources | |
return file_accessors.flat_map do |accessor| accessor.resources end.map do |path| "#{path.basename}" end | |
end | |
def vendor_products | |
return file_accessors.flat_map do |accessor| | |
accessor.vendored_frameworks + accessor.vendored_libraries | |
end.map do |s| s.basename | |
end.map do |s| | |
name = "#{s}" | |
if name.include? "framework" | |
PodDependency.newFramework name.sub(".framework", "") | |
else | |
PodDependency.newLibrary name.sub("lib", "").sub(".a", "") | |
end | |
end | |
end | |
def frameworks | |
return file_accessors.flat_map do |accessor| | |
accessor.spec_consumer.frameworks.map do |name| PodDependency.newFramework name end + accessor.spec_consumer.libraries.map do |name| PodDependency.newLibrary name end | |
end | |
end | |
end | |
class PBXTargetDependency | |
def module_name | |
string = name.clone.gsub! /-iOS[0-9]+(\.[0-9])+$/, '' | |
return string.nil? ? name : string | |
end | |
end | |
class AbstractTarget | |
def module_name | |
string = name.clone.gsub! /-iOS[0-9]+(\.[0-9])+$/, '' | |
return string.nil? ? name : string | |
end | |
###### STEP 2 ###### | |
# In all targets (aggregates + native), filter dependencies | |
def add_platform_filter_to_dependencies platform | |
loggs "\t\t- Filtering dependencies" | |
dependencies.each do |dependency| | |
dependency.platform_filter = platform.name | |
end | |
end | |
###### STEP 3 ###### | |
# If any unsupported library, then flag as platform-dependant for every build configuration | |
def flag_libraries libraries, platform | |
loggs "\tTarget: #{name}" | |
build_configurations.filter do |config| not config.base_configuration_reference.nil? | |
end.each do |config| | |
loggs "\t\tScheme: #{config.name}" | |
xcconfig_path = config.base_configuration_reference.real_path | |
xcconfig = File.read(xcconfig_path) | |
changed = false | |
libraries.each do |framework| | |
if xcconfig.include? framework | |
xcconfig.gsub!(framework, '') | |
unless xcconfig.include? "OTHER_LDFLAGS[sdk=#{platform.sdk}]" | |
changed = true | |
xcconfig += "OTHER_LDFLAGS[sdk=#{platform.sdk}] = $(inherited) -ObjC " | |
end | |
xcconfig += framework + ' ' | |
end | |
end | |
File.open(xcconfig_path, "w") { |file| file << xcconfig } | |
loggs "\t\t\t#{changed ? "Succeded" : "Nothing to flag"}" | |
end | |
end | |
def to_dependency | |
# We return both as we don't know if build as library or framework | |
return [PodDependency.newFramework(module_name), PodDependency.newLibrary(module_name)] | |
end | |
# Dependencies contained in Other Linker Flags | |
def other_linker_flags_dependencies | |
frameworks = Array.new | |
libraries = Array.new | |
config = build_configurations.filter do |config| not config.base_configuration_reference.nil? end.first | |
xcconfig_path = config.base_configuration_reference.real_path | |
xcconfig = File.read(xcconfig_path) | |
xcconfig.gsub!(/\r\n?/, "\n") | |
xcconfig.each_line do |line| | |
if line.start_with? 'OTHER_LDFLAGS' | |
frameworks = frameworks + line.split("-framework").map do |s| | |
s.strip.delete("\n") end.filter do |s| | |
s.strip.start_with? '"' end | |
libraries = libraries + line.split("-l").filter do |s| s.strip.start_with? '"' end.map do |s| s.strip.split(' ').first end | |
end | |
end | |
libraries = libraries.map do |name| PodDependency.newLibrary(name.gsub!("\"", "")) end | |
frameworks = frameworks.map do |name| PodDependency.newFramework(name.gsub!("\"", "")) end | |
return OtherLinkerFlagsDependencies.new libraries, frameworks | |
end | |
end | |
# HELPER CLASSES | |
class PodDependency | |
attr_reader :name | |
attr_reader :type | |
def link | |
if library? | |
return "-l\"#{name}\"" | |
else | |
return "-framework \"#{name}\"" | |
end | |
end | |
def library? | |
return type == "library" | |
end | |
def framework? | |
return type == "framework" | |
end | |
def self.newFramework name | |
return PodDependency.new(name, "framework") | |
end | |
def self.newLibrary name | |
return PodDependency.new(name, "library") | |
end | |
def ==(other) | |
name == other.name && type == other.type | |
end | |
def eql?(other) | |
name == other.name | |
end | |
private | |
def initialize(name, type) | |
@name = name | |
@type = type | |
end | |
end | |
class OtherLinkerFlagsDependencies | |
attr_reader :libraries | |
attr_reader :frameworks | |
def initialize(libraries = [], frameworks = []) | |
@libraries = libraries | |
@frameworks = frameworks | |
end | |
def combine dependencies | |
frameworks = (dependencies.frameworks + @frameworks).to_set.to_a | |
libraries = (dependencies.libraries + @libraries).to_set.to_a | |
return OtherLinkerFlagsDependencies.new libraries, frameworks | |
end | |
def dependencies | |
libraries + frameworks | |
end | |
end | |
class OSPlatform | |
attr_reader :sdk | |
attr_reader :name | |
attr_reader :architectures | |
def self.ios | |
OSPlatform.new 'ios', 'iphone*', ['arm64', 'armv7s', 'armv7'] | |
end | |
def self.macos | |
OSPlatform.new 'macos', 'macosx*', ['x86_64'] | |
end | |
def self.wtachos | |
OSPlatform.new 'watchos', 'watchos*', ['arm64_32', 'armv7k'] | |
end | |
def self.tvos | |
OSPlatform.new 'tvos', 'appletvos*', ['arm64'] | |
end | |
private | |
def initialize(name, sdk, architectures) | |
@name = name | |
@sdk = sdk | |
@architectures = architectures | |
end | |
end | |
# SCRIPT | |
class Installer | |
def configure_support_catalyst pod_names_to_keep, pod_names_to_remove, configurations=nil | |
###### Variable definition ###### | |
targets = pods_project.targets | |
pod_names_to_remove = pod_names_to_remove.map do |name| name.sub('/', '') end | |
pod_names_to_keep = pod_names_to_keep.map do |name| name.sub('/', '') end | |
pod_names_to_keep = recursive_dependencies pod_names_to_keep | |
pod_names_to_remove = recursive_dependencies(pod_names_to_remove).filter do |name| !pod_names_to_keep.include? name end | |
pod_targets_to_keep = pod_targets.filter do |pod| pod_names_to_keep.include? pod.module_name end # PodTarget | |
pod_targets_to_remove = pod_targets.filter do |pod| pod_names_to_remove.include? pod.module_name end # PodTarget | |
loggs "\n#### Unsupported Libraries ####\n#{pod_names_to_remove}\n" | |
targets_to_remove = targets.filter do |target| pod_names_to_remove.include?(target.module_name) end # AbstractTarget | |
pods_targets = targets.filter do |target| target.name.start_with? "Pods-" end # AbstractTarget | |
cross_platform_targets = targets.filter do |target| !targets_to_remove.include?(target) && !pods_targets.include?(target) end # AbstractTarget | |
###### Determine which dependencies should be removed ###### | |
dependencies_to_keep = cross_platform_targets.reduce(OtherLinkerFlagsDependencies.new) do |dependencies, target| | |
dependencies.combine target.other_linker_flags_dependencies | |
end.dependencies | |
# [PodDependency] | |
dependencies_to_keep = dependencies_to_keep + cross_platform_targets.flat_map do |target| target.to_dependency end + pod_targets_to_keep.flat_map do |pod| pod.vendor_products + pod.frameworks end | |
dependencies_to_remove = targets_to_remove.reduce(OtherLinkerFlagsDependencies.new) do |dependencies, target| | |
dependencies.combine target.other_linker_flags_dependencies | |
end.dependencies | |
# [PodDependency] | |
dependencies_to_remove = dependencies_to_remove + targets_to_remove.flat_map do |target| target.to_dependency end + pod_targets_to_remove.flat_map do |pod| pod.vendor_products + pod.frameworks end | |
dependencies_to_remove = dependencies_to_remove.filter do |d| !dependencies_to_keep.include? d end | |
###### CATALYST NOT SUPPORTED LINKS ###### | |
unsupported_links = dependencies_to_remove.map do |d| d.link end.to_set.to_a | |
loggs "#### Unsupported dependencies ####\n" | |
loggs "#{dependencies_to_remove.map do |d| d.name end.to_set.to_a }\n\n" | |
###### CATALYST NOT SUPPORTED FRAMEWORKS AND RESOURCES | |
frameworks_to_uninstall = dependencies_to_remove.filter do |d| d.framework? end.map do |d| "#{d.name}.framework" end.to_set.to_a | |
resources_to_uninstall = pod_targets_to_remove.flat_map do |pod| pod.resources end.to_set.to_a | |
loggs "#### Frameworks not to be included in the Archive ####\n" | |
loggs "#{frameworks_to_uninstall}\n\n" | |
loggs "#### Resources not to be included in the Archive ####\n" | |
loggs "#{resources_to_uninstall}\n\n" | |
###### OTHER LINKER FLAGS -> to iphone* ###### | |
loggs "#### Flagging unsupported libraries ####" | |
targets.each do |target| target.flag_libraries unsupported_links, OSPlatform.ios end | |
###### BUILD_PHASES AND DEPENDENCIES -> PLATFORM_FILTER 'ios' ###### | |
loggs "\n#### Filtering build phases ####" | |
targets_to_remove.filter do |target| | |
pods_project.native_targets.include? target | |
end.each do |target| | |
loggs "\tTarget: #{target.name}" | |
target.add_platform_filter_to_build_phases OSPlatform.ios | |
target.add_platform_filter_to_dependencies OSPlatform.ios | |
end | |
loggs "\n#### Filtering dependencies ####" | |
targets_to_remove.filter do |target| | |
!pods_project.native_targets.include? target | |
end.each do |target| | |
loggs "\tTarget: #{target.name}" | |
target.add_platform_filter_to_dependencies OSPlatform.ios | |
end | |
###### FRAMEWORKS AND RESOURCES SCRIPT -> if [ "$ARCHS" != "x86_64" ]; then ####### | |
loggs "\n#### Chagings frameworks and resources script ####" | |
pods_targets.each do |target| | |
loggs "\tTarget: #{target.name}" | |
loggs "\t\t-Uninstalling frameworks" | |
target.uninstall_frameworks frameworks_to_uninstall, OSPlatform.macos, configurations | |
loggs "\t\t-Uninstalling resources" | |
target.uninstall_resources resources_to_uninstall, OSPlatform.macos, configurations | |
end | |
end | |
@private | |
def recursive_dependencies to_filter_names | |
targets = pods_project.targets | |
targets_to_remove = targets.filter do |target| to_filter_names.include? target.module_name end | |
dependencies = targets_to_remove.flat_map do |target| target.dependencies end | |
dependencies_names = dependencies.map do |d| d.module_name end | |
if dependencies.empty? | |
return to_filter_names + dependencies_names | |
else | |
return to_filter_names + recursive_dependencies(dependencies_names) | |
end | |
end | |
end |
This comment has been minimized.
This comment has been minimized.
Thanks, I appreciate it. That sounds weird, I’d swear Cocoapods scripts strip the frameworks, here I’m just unlinking them. Maybe I’m missing something. I have my App uploaded to the App Store and works like a charm. I just have some warnings symbol-related. I’ll dig into this, thanks a lot for your feedback. |
This comment has been minimized.
This comment has been minimized.
Hi again, Can you try adding this after line 168?
You can try commenting or uncommenting |
This comment has been minimized.
This comment has been minimized.
Sure, I'll give it a try and let you know. |
This comment has been minimized.
This comment has been minimized.
Doesn't seem to work: |
This comment has been minimized.
This comment has been minimized.
Hi, I tried your script on GoogleAppMeasurement, it works and removes the Mac Catalyst error, but it gives two new errors: Any thoughts on how to fix that? Thanks. |
This comment has been minimized.
This comment has been minimized.
Hi @Phacometer, can you share your Podfile please? |
This comment has been minimized.
This comment has been minimized.
Hi @henrytkirk, I'm trying to figure out a way to unlink that bundle. The problem is it doesn't appear in GoogleSignIn's Build phase. What you could do is maybe when you're going to upload an IPA to the App Store, have Fastlane (or other tool) or manually delete that bundle from your ipa. So export it and then delete this bundle or do so directly in the project. I'm still seeing what it can be done |
This comment has been minimized.
This comment has been minimized.
Hi @fermoya - yes, I'm doing that manually for now. I'm not blocked from submitting anymore. Thanks for trying to fix this issue and providing this script. |
This comment has been minimized.
This comment has been minimized.
Hi @fermoya, here is my Podfile:
|
This comment has been minimized.
This comment has been minimized.
Hi @Phacometer, Unfortunately, this isn't how the script is thought to work. If a framework isn't compatible with MacOS, you need to "unlink" that framework, not a dependency. Otherwise, it won't compile. So it should be:
This will configure this framework and its dependencies so that they're not link for macOS. Anyway, I've installed |
This comment has been minimized.
This comment has been minimized.
Hi @fermoya, I removed Google Mobile Ads SDK from unsupported_pods and updated the pods with |
This comment has been minimized.
This comment has been minimized.
Hi @henrytkirk, Good to hear you're not blocked. If you check your project folder, you should have a file called
You can swap if with this:
This will install the resource in your bundle only if it's not a macOS architecture. That should do the trick. Notice if you run |
This comment has been minimized.
This comment has been minimized.
Hi @Phacometer, Sorry, you're right it doesn't compile. All in all, the pod you're installing is So, |
This comment has been minimized.
This comment has been minimized.
Hi @henrytkirk, I've updated the script to automatically remove bundle resources that could come with the Pod. I've been trying it with Best |
This comment has been minimized.
This comment has been minimized.
@fermoya,
The way I'm calling the script in my Podfile is as follows:
|
This comment has been minimized.
This comment has been minimized.
Hi @henrytkirk, That’s right, I added it 2 weeks ago I think. I noticed that dependencies might be shared between pods and I would be wrongly unlinking them if needed for a cross-platform pod. So the script needs to know the pods you’re installing that support macOS architectures and the pods that don’t support macOS architectures. |
This comment has been minimized.
This comment has been minimized.
Ok, I guess it's not too bad to manage those lists. My project has a lot of targets and pods. Thanks! |
This comment has been minimized.
This comment has been minimized.
@henrytkirk, let me know if you find any issue. So the last additions are:
|
This comment has been minimized.
This comment has been minimized.
So far so good - except this CocoaPod fails, and I can't figure out how to resolve: Firebase Analytics:
Ideas? This used to work before, not sure if it's a change in Firebase or the script. |
This comment has been minimized.
This comment has been minimized.
I wonder if it's this umbrella header: I'll try manually adding the ones I need. |
This comment has been minimized.
This comment has been minimized.
Hi @henrytkirk, How are you using Firebase pods? I'm using them too and I'm not having that problem. Notice Firebase/Analytics contains pods that aren't compiled for macOS. |
This comment has been minimized.
This comment has been minimized.
Firebase Analytics isn't supported in Catalyst. I'm also using Auth, Core, Remote Config, etc. It was working fine about two-three weeks ago. Which ones are you using? What does your pod file look like? |
This comment has been minimized.
This comment has been minimized.
|
This comment has been minimized.
This comment has been minimized.
Still can't get this to work correctly. Keeps failing on Here is are my supported/unsupported pods so far:
Any ideas? Could the script be missing something? |
This comment has been minimized.
This comment has been minimized.
Hi @henrytkirk, can you look up "FIRAnalyticsConnector.framework" in your project and verify is always under "OTHER_LDFLAGS[sdk=iphone*]"? |
This comment has been minimized.
This comment has been minimized.
@fermoya, sorry I'm not familiar with how to identify that. Can you provide some steps? |
This comment has been minimized.
This comment has been minimized.
@fermoya, if you're speaking about the pod .xcconfig, here's the FirebaseAnalytics one:
|
This comment has been minimized.
This comment has been minimized.
Hi @henrytkirk, I meant you should see something like this:
In "Pod-YourAppName.debug.xcconfig". I just created a new project, I'm installing the libraries you mentioned and it building fine, I'm sharing the Podfile below. Notice I'm explicitly using a version for Firebase pods, 6.23.0. This is because Cocoapods was for some reason installing Firebase.h 3.6.0 for me, which is wrong. If you try using 6.24.0 or 6.25.0 you'll find the error:
I guess they'll be looking into this now. The PodFile is this:
|
This comment has been minimized.
This comment has been minimized.
None of my Cocoapods xcconfig files have "OTHER_LDFLAGS[sdk=iphone*]". How is that set? |
This comment has been minimized.
This comment has been minimized.
@henrytkirk, you need to go to your project folder and run |
This comment has been minimized.
This comment has been minimized.
Yes, that's how I've been doing it. |
This comment has been minimized.
This comment has been minimized.
@henrytkirk, do you maybe want to schedule a Skype call? Reach me out at fmdr.ct@gmail.com |
This comment has been minimized.
This comment has been minimized.
@fermoya have you used this with the unsupported pod |
This comment has been minimized.
This comment has been minimized.
@Brett-Best, no I haven't. When you do Please, refer to CatalystPodSupport in the future |
This comment has been minimized.
This comment has been minimized.
I don't understand, I'm trying to add a simple pod: please help |
This comment has been minimized.
This comment has been minimized.
Hi @afern247, this script is outdated. Please visit https://github.com/fermoya/CatalystPodSupport |
This comment has been minimized.
This works really great, thanks for writing this! One issue I found is it doesn't strip resources that a Framework may contain. For example, GoogleSignIn.bundle is still present from the 'GoogleSignIn' pod. It doesn't prevent it from building but prevents it from uploading to App Store Connect.