Skip to content

Instantly share code, notes, and snippets.

@fermoya
Last active September 4, 2023 09:06
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fermoya/f9be855ad040d5545ae3cb254ed201e4 to your computer and use it in GitHub Desktop.
Save fermoya/f9be855ad040d5545ae3cb254ed201e4 to your computer and use it in GitHub Desktop.
###### 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
@fermoya
Copy link
Author

fermoya commented Dec 22, 2020

Hi @afern247, this script is outdated. Please visit https://github.com/fermoya/CatalystPodSupport

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment