mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-11 22:55:59 +05:45
* Rename utils.cmake to cc_rules.cmake This makes it less of a dumping ground * Fix sign mismatch in FIRApp * Implement a podspec_framework CMake function ... that generates a CMake framework library target from a podspec. * Remove manual CMake scripts for xcodebuild
496 lines
14 KiB
Ruby
Executable File
496 lines
14 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
# Copyright 2018 Google
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
require 'cocoapods'
|
|
require 'fileutils'
|
|
require 'pathname'
|
|
require 'set'
|
|
|
|
PLATFORM = :osx
|
|
|
|
def usage()
|
|
script = File.basename($0)
|
|
STDERR.puts <<~EOF
|
|
USAGE: #{script} podspec cmake-file [subspecs...]
|
|
EOF
|
|
end
|
|
|
|
def main(args)
|
|
if args.size < 2 then
|
|
usage()
|
|
exit(1)
|
|
end
|
|
|
|
process(*args)
|
|
end
|
|
|
|
# A CMake command, like add_library. The command name is stored in the first
|
|
# argument.
|
|
class CMakeCommand
|
|
# Create the command with its initial identifying arguments.
|
|
def initialize(*args)
|
|
@args = args
|
|
@checked_count = 0
|
|
end
|
|
|
|
def name()
|
|
return @args[0]
|
|
end
|
|
|
|
def rest()
|
|
return @args[1..-1]
|
|
end
|
|
|
|
def skip?()
|
|
return @checked_count == 0
|
|
end
|
|
|
|
def allow_missing_args()
|
|
@checked_count = nil
|
|
end
|
|
|
|
# Adds the given arguments to the end of the command
|
|
def add_args(*args)
|
|
args = args.flatten
|
|
|
|
unless @checked_count.nil?
|
|
@checked_count += args.size
|
|
end
|
|
|
|
args.each do |arg|
|
|
unless @args.include?(arg)
|
|
@args.push(arg)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# A model of a macOS or iOS Framework and the CMake commands required to build
|
|
# it.
|
|
class Framework
|
|
def initialize(name)
|
|
@name = name
|
|
@add_library = CMakeCommand.new('add_library', @name, 'STATIC')
|
|
@add_library.allow_missing_args()
|
|
|
|
@public_headers = CMakeCommand.new(
|
|
'set_property', 'TARGET', @name, 'PROPERTY', 'PUBLIC_HEADER')
|
|
|
|
@properties = []
|
|
|
|
@extras = {}
|
|
end
|
|
|
|
# Returns all the CMake commands required to build the framework.
|
|
def commands()
|
|
result = [@add_library]
|
|
result.push(@public_headers, *@properties)
|
|
@extras.keys.sort.each do |key|
|
|
result.push(@extras[key])
|
|
end
|
|
return result
|
|
end
|
|
|
|
# Adds library sources to the CMake add_library command that declares the
|
|
# library.
|
|
def add_sources(*sources)
|
|
@add_library.add_args(sources)
|
|
end
|
|
|
|
# Adds public headers to the Framework
|
|
def add_public_headers(*headers)
|
|
@public_headers.add_args(headers)
|
|
end
|
|
|
|
# Sets a target-level CMake property on the library target that declares the
|
|
# framework.
|
|
def set_property(property, *values)
|
|
command = CMakeCommand.new('set_property', 'TARGET', @name, 'PROPERTY', property)
|
|
command.add_args(values)
|
|
@properties.push(command)
|
|
end
|
|
|
|
# Adds target-level preprocessor definitions.
|
|
#
|
|
# Args:
|
|
# - type: PUBLIC, PRIVATE, or INTERFACE
|
|
# - values: C preprocessor defintion arguments starting with -D
|
|
def compile_definitions(type, *values)
|
|
extra_command('target_compile_definitions', @name, type)
|
|
.add_args(values)
|
|
end
|
|
|
|
# Adds target-level compile-time include path for the preprocessor.
|
|
#
|
|
# Args:
|
|
# - type: PUBLIC, PRIVATE, or INTERFACE
|
|
# - values: directory names, not including a leading -I flag
|
|
def include_directories(type, *dirs)
|
|
extra_command('target_include_directories', @name, type)
|
|
.add_args(dirs)
|
|
end
|
|
|
|
# Adds target-level compile-time compiler options that aren't macro
|
|
# definitions or include directories. Link-time options should be added via
|
|
# lib_libraries.
|
|
#
|
|
# Args:
|
|
# - type: PUBLIC, PRIVATE, or INTERFACE
|
|
# - values: compiler flags, e.g. -fno-autolink
|
|
def compile_options(type, *values)
|
|
extra_command('target_compile_options', @name, type)
|
|
.add_args(values)
|
|
end
|
|
|
|
# Adds target-level dependencies or link-time compiler options. CMake
|
|
# interprets any quoted string that starts with "-" as an option and anything
|
|
# else as a library target to depend upon.
|
|
#
|
|
# Args:
|
|
# - type: PUBLIC, PRIVATE, or INTERFACE
|
|
# - values: compiler flags, e.g. -fno-autolink
|
|
def link_libraries(type, *dirs)
|
|
extra_command('target_link_libraries', @name, type)
|
|
.add_args(dirs)
|
|
end
|
|
|
|
private
|
|
def extra_command(*key_args)
|
|
key = key_args.join('|')
|
|
command = @extras[key]
|
|
if command.nil?
|
|
command = CMakeCommand.new(*key_args)
|
|
@extras[key] = command
|
|
end
|
|
return command
|
|
end
|
|
end
|
|
|
|
# Generates a framework target based on podspec contents. Models the translation
|
|
# of a single podspec (and possible subspecs) to a single CMake framework
|
|
# target.
|
|
class CMakeGenerator
|
|
|
|
# Initializes the generator with the given root Pod::Spec and the binary
|
|
# directory for the current CMake configuration.
|
|
#
|
|
# Args:
|
|
# - spec: A root specification, the name of which becomes the name of the
|
|
# Framework.
|
|
# - path_list: A Pod::Sandbox::PathList used to cache file operations.
|
|
# - cmake_binary_dir: A directory in which additional files may be written.
|
|
def initialize(spec, path_list, cmake_binary_dir)
|
|
@target = Framework.new(spec.name)
|
|
|
|
headers_root = File.join(cmake_binary_dir, 'Headers')
|
|
@headers_dir = File.join(headers_root, spec.name)
|
|
|
|
@root = spec
|
|
|
|
@target.set_property('FRAMEWORK', 'ON')
|
|
@target.set_property('VERSION', spec.version)
|
|
|
|
@target.include_directories('PRIVATE', headers_root, @headers_dir)
|
|
@target.link_libraries('PUBLIC', "\"-framework Foundation\"")
|
|
|
|
root_dir = Pathname.new(__FILE__).expand_path().dirname().dirname()
|
|
@path_list = Pod::Sandbox::PathList.new(root_dir)
|
|
end
|
|
|
|
attr_reader :target
|
|
|
|
# Adds information from the given Pod::Spec to the definition of the CMake
|
|
# framework target. Subspecs are not automatically handled.
|
|
#
|
|
# Cocoapods subspecs are not independent libraries--they contribute sources
|
|
# and dependencies to a final single Framework.
|
|
#
|
|
# Args:
|
|
# - spec: A root or subspec that contributes to the final state of the of the
|
|
# Framework.
|
|
def add_framework(spec)
|
|
spec = spec.consumer(PLATFORM)
|
|
files = Pod::Sandbox::FileAccessor.new(@path_list, spec)
|
|
sources = [
|
|
files.source_files,
|
|
files.public_headers,
|
|
files.private_headers,
|
|
].flatten
|
|
@target.add_sources(sources)
|
|
|
|
add_headers(files, sources)
|
|
|
|
add_dependencies(spec)
|
|
add_framework_dependencies(spec)
|
|
|
|
@target.compile_options('INTERFACE', '-F${CMAKE_CURRENT_BINARY_DIR}')
|
|
@target.compile_options('PRIVATE', '${OBJC_FLAGS}')
|
|
|
|
add_xcconfig('PRIVATE', spec.pod_target_xcconfig)
|
|
add_xcconfig('PUBLIC', spec.user_target_xcconfig)
|
|
end
|
|
|
|
private
|
|
# Sets up the framework headers so that compilation can succeed.
|
|
# Xcode/CocoaPods allow for several different include mechanisms to work:
|
|
#
|
|
# * Unqualified headers, e.g. +#import "FIRLoggerLevel.h"+, typically
|
|
# resolved via the header map.
|
|
# * Qualified relative to some source root, e.g.
|
|
# +#import "Public/FIRLoggerLevel.h"+, typically resolved by an include
|
|
# path
|
|
# * Framework imports, e.g. +#import <FirebaseCore/FIRLoggerLevel.h>+,
|
|
# resolved by a build process that copies headers into the framework
|
|
# structure.
|
|
# * Umbrella imports e.g. +#import <FirebaseCore/FirebaseCore.h>+ (which
|
|
# happens to import all the public headers).
|
|
#
|
|
# CMake's framework support is incomplete. It has no support at all for
|
|
# generating umbrella headers.
|
|
#
|
|
# CMake also does not completely support framework imports. It does work for
|
|
# sources outside the framework that want to build against it, but until the
|
|
# framework has been completely built the headers aren't available in this
|
|
# form. This prevents frameworks from referring to their own code via
|
|
# framework imports.
|
|
#
|
|
# This method cheats by creating a subdirectory in the build results that has
|
|
# symbolic links of all the public headers accessible with the right path.
|
|
# This makes it possible to use framework imports within the framework itself.
|
|
# The parent of this path is then added as a PRIVATE include directory of the
|
|
# target, making it possible for the framework to see itself this way.
|
|
def add_headers(files, sources)
|
|
# CMake-built frameworks don't have a notion of private headers, but they
|
|
# also don't have a notion of umbrella headers, so all framework headers
|
|
# need to be accessed by name. This means that just dumping all the private
|
|
# and public headers into what CMake considers the public headers makes
|
|
# everything work as we expect.
|
|
headers = [
|
|
files.public_headers,
|
|
files.private_headers
|
|
].flatten
|
|
|
|
@target.add_public_headers(headers)
|
|
|
|
# Also, link the headers into a directory that looks like a framework layout
|
|
# so that self-references via framework imports work. These *must* be
|
|
# symbolic links, otherwise our usual sloppiness causes file contents to be
|
|
# included multiple times, usually resulting in ambiguity errors.
|
|
FileUtils.mkdir_p(@headers_dir)
|
|
headers.each do |header|
|
|
FileUtils.ln_sf(header, File.join(@headers_dir, File.basename(header)))
|
|
end
|
|
|
|
# Simulate header maps by adding include paths for all the directories
|
|
# containing non-public headers.
|
|
hmap_dirs = Set.new()
|
|
sources.each do |source|
|
|
next if File.extname(source) != '.h'
|
|
next if headers.include?(source)
|
|
|
|
hmap_dirs.add(File.dirname(source))
|
|
end
|
|
@target.include_directories('PRIVATE', hmap_dirs.to_a.sort)
|
|
end
|
|
|
|
# Adds Pod::Spec +dependencies+ as target_link_libraries. Only root-specs are
|
|
# added as dependencies because in the CMake build there can be only one
|
|
# target for the framework.
|
|
def add_dependencies(spec)
|
|
prefix = "#{@root.name}/"
|
|
spec.dependencies.each do |dep|
|
|
# Dependencies on subspecs of this same spec are handled elsewhere.
|
|
next if dep.name.start_with?(prefix)
|
|
|
|
name = dep.name.sub(/\/.*/, '')
|
|
@target.link_libraries('PUBLIC', name)
|
|
end
|
|
end
|
|
|
|
# Adds target_link_libraries entries for all the items in the Pod::Spec
|
|
# +frameworks+ attribute.
|
|
def add_framework_dependencies(spec)
|
|
spec.frameworks.each do |framework|
|
|
@target.link_libraries('PUBLIC', "\"-framework #{framework}\"")
|
|
end
|
|
end
|
|
|
|
# Mirrors known entries from the xcconfig entries into their equivalents in
|
|
# CMake. This translates OTHER_CFLAGS, GCC_PREPROCESSOR_DEFINITIONS, and
|
|
# HEADER_SEARCH_PATHS.
|
|
#
|
|
# Args:
|
|
# - type: PUBLIC for +pod_user_xcconfig+ or PRIVATE for
|
|
# +pod_target_xcconfig+.
|
|
# - xcconfig: the hash of xcconfig values.
|
|
def add_xcconfig(type, xcconfig)
|
|
if xcconfig.empty?
|
|
return
|
|
end
|
|
|
|
@target.compile_options(type, split(xcconfig['OTHER_CFLAGS']))
|
|
|
|
defs = split(xcconfig['GCC_PREPROCESSOR_DEFINITIONS'])
|
|
defs = defs.map { |x| '-D' + x }
|
|
@target.compile_definitions(type, defs)
|
|
|
|
@target.include_directories(type, split(xcconfig['HEADER_SEARCH_PATHS']))
|
|
end
|
|
|
|
# Splits a textual value in xcconfig. Always returns an array, but that array
|
|
# may be empty if the value didn't exist in the podspec.
|
|
def split(value)
|
|
if value.nil?
|
|
return []
|
|
elsif value.kind_of?(String)
|
|
return value.split
|
|
else
|
|
return [value]
|
|
end
|
|
end
|
|
end
|
|
|
|
# Processes a podspec file, translating all the specs within it into cmake file
|
|
# describing how to build it.
|
|
#
|
|
# Args:
|
|
# - podspec_file: The filename of the podspec to use as a source.
|
|
# - cmake_file: The filename of the cmake script to produce.
|
|
# - req_subspecs: Which subspecs to include. If empty, all subspecs are
|
|
# included (which corresponds to CocoaPods behavior. The default_subspec
|
|
# property is not handled.
|
|
def process(podspec_file, cmake_file, *req_subspecs)
|
|
root_dir = Pathname.new(__FILE__).expand_path().dirname().dirname()
|
|
path_list = Pod::Sandbox::PathList.new(root_dir)
|
|
|
|
spec = Pod::Spec.from_file(podspec_file)
|
|
|
|
writer = Writer.new()
|
|
writer.append <<~EOF
|
|
# This file was generated by #{File.basename(__FILE__)}
|
|
# from #{File.basename(podspec_file)}.
|
|
# Do not edit!
|
|
EOF
|
|
|
|
cmake_binary_dir = File.expand_path(File.dirname(cmake_file))
|
|
|
|
gen = CMakeGenerator.new(spec, path_list, cmake_binary_dir)
|
|
gen.add_framework(spec)
|
|
|
|
req_subspecs = normalize_requested_subspecs(spec, req_subspecs)
|
|
req_subspecs = resolve_subspec_deps(spec, req_subspecs)
|
|
|
|
spec.subspecs.each do |subspec|
|
|
if req_subspecs.include?(subspec.name)
|
|
gen.add_framework(subspec)
|
|
end
|
|
end
|
|
|
|
gen.target.commands.each do |command|
|
|
writer.write(command)
|
|
end
|
|
|
|
File.open(cmake_file, 'w') do |fd|
|
|
fd.write(writer.result)
|
|
end
|
|
end
|
|
|
|
# Translates the (possibly empty) list of requested subspecs into the list of
|
|
# subspecs to actually include. If +req_subspecs+ is empty, returns all
|
|
# subspecs. If non-empty, all subspecs are returned as qualified names, e.g.
|
|
# "Logger" may become "GoogleUtilities/Logger".
|
|
def normalize_requested_subspecs(spec, req_subspecs)
|
|
subspecs = spec.subspecs
|
|
if req_subspecs.empty?
|
|
return subspecs.map { |s| s.name }
|
|
else
|
|
return req_subspecs.map do |name|
|
|
if name.include?(?/)
|
|
name
|
|
else
|
|
"#{spec.name}/#{name}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Expands the list of requested subspecs to include any dependencies within the
|
|
# same root subspec. For example, if +req_subspecs+ where
|
|
#
|
|
# +["GoogleUtilties/Logger"]+,
|
|
#
|
|
# the result would be
|
|
#
|
|
# +["GoogleUtilties/Logger", "GoogleUtilities/Environment"]+
|
|
#
|
|
# because Logger depends upon Environment within the same root spec.
|
|
def resolve_subspec_deps(spec, req_subspecs)
|
|
prefix = spec.name + '/'
|
|
|
|
result = Set.new()
|
|
while !req_subspecs.empty?
|
|
req = req_subspecs.pop
|
|
result.add(req)
|
|
|
|
subspec = spec.subspec_by_name(req)
|
|
subspec.dependencies(PLATFORM).each do |dep|
|
|
if dep.name.start_with?(prefix) && !result.include?(dep.name)
|
|
req_subspecs.push(dep.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
return result.to_a.sort
|
|
end
|
|
|
|
# Writes CMake commands out to textual form, taking care of line wrapping.
|
|
class Writer
|
|
def initialize()
|
|
@last_command = nil
|
|
@result = ""
|
|
end
|
|
|
|
attr_reader :result
|
|
|
|
def write(command)
|
|
if command.skip?
|
|
return
|
|
end
|
|
|
|
if command.name != @last_command
|
|
@result << "\n"
|
|
end
|
|
@last_command = command.name
|
|
|
|
single = "#{command.name}(#{command.rest.join(' ')})\n"
|
|
if single.size < 80
|
|
@result << single
|
|
else
|
|
@result << "#{command.name}(\n"
|
|
command.rest.each do |arg|
|
|
@result << " #{arg}\n"
|
|
end
|
|
@result << ")\n"
|
|
end
|
|
end
|
|
|
|
def append(text)
|
|
@result << text
|
|
end
|
|
end
|
|
|
|
main(ARGV)
|