aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmake/podspec_cmake.rb
diff options
context:
space:
mode:
Diffstat (limited to 'cmake/podspec_cmake.rb')
-rwxr-xr-xcmake/podspec_cmake.rb495
1 files changed, 495 insertions, 0 deletions
diff --git a/cmake/podspec_cmake.rb b/cmake/podspec_cmake.rb
new file mode 100755
index 0000000..4063f35
--- /dev/null
+++ b/cmake/podspec_cmake.rb
@@ -0,0 +1,495 @@
+#!/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)