podspec_cmake.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. #!/usr/bin/env ruby
  2. # Copyright 2018 Google
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. require 'cocoapods'
  16. require 'fileutils'
  17. require 'pathname'
  18. require 'set'
  19. PLATFORM = :osx
  20. def usage()
  21. script = File.basename($0)
  22. STDERR.puts <<~EOF
  23. USAGE: #{script} podspec cmake-file [subspecs...]
  24. EOF
  25. end
  26. def main(args)
  27. if args.size < 2 then
  28. usage()
  29. exit(1)
  30. end
  31. process(*args)
  32. end
  33. # A CMake command, like add_library. The command name is stored in the first
  34. # argument.
  35. class CMakeCommand
  36. # Create the command with its initial identifying arguments.
  37. def initialize(*args)
  38. @args = args
  39. @checked_count = 0
  40. end
  41. def name()
  42. return @args[0]
  43. end
  44. def rest()
  45. return @args[1..-1]
  46. end
  47. def skip?()
  48. return @checked_count == 0
  49. end
  50. def allow_missing_args()
  51. @checked_count = nil
  52. end
  53. # Adds the given arguments to the end of the command
  54. def add_args(*args)
  55. args = args.flatten
  56. unless @checked_count.nil?
  57. @checked_count += args.size
  58. end
  59. args.each do |arg|
  60. unless @args.include?(arg)
  61. @args.push(arg)
  62. end
  63. end
  64. end
  65. end
  66. # A model of a macOS or iOS Framework and the CMake commands required to build
  67. # it.
  68. class Framework
  69. def initialize(name)
  70. @name = name
  71. @add_library = CMakeCommand.new('add_library', @name, 'STATIC')
  72. @add_library.allow_missing_args()
  73. @public_headers = CMakeCommand.new(
  74. 'set_property', 'TARGET', @name, 'PROPERTY', 'PUBLIC_HEADER')
  75. @properties = []
  76. @extras = {}
  77. end
  78. # Returns all the CMake commands required to build the framework.
  79. def commands()
  80. result = [@add_library]
  81. result.push(@public_headers, *@properties)
  82. @extras.keys.sort.each do |key|
  83. result.push(@extras[key])
  84. end
  85. return result
  86. end
  87. # Adds library sources to the CMake add_library command that declares the
  88. # library.
  89. def add_sources(*sources)
  90. @add_library.add_args(sources)
  91. end
  92. # Adds public headers to the Framework
  93. def add_public_headers(*headers)
  94. @public_headers.add_args(headers)
  95. end
  96. # Sets a target-level CMake property on the library target that declares the
  97. # framework.
  98. def set_property(property, *values)
  99. command = CMakeCommand.new('set_property', 'TARGET', @name, 'PROPERTY', property)
  100. command.add_args(values)
  101. @properties.push(command)
  102. end
  103. # Adds target-level preprocessor definitions.
  104. #
  105. # Args:
  106. # - type: PUBLIC, PRIVATE, or INTERFACE
  107. # - values: C preprocessor defintion arguments starting with -D
  108. def compile_definitions(type, *values)
  109. extra_command('target_compile_definitions', @name, type)
  110. .add_args(values)
  111. end
  112. # Adds target-level compile-time include path for the preprocessor.
  113. #
  114. # Args:
  115. # - type: PUBLIC, PRIVATE, or INTERFACE
  116. # - values: directory names, not including a leading -I flag
  117. def include_directories(type, *dirs)
  118. extra_command('target_include_directories', @name, type)
  119. .add_args(dirs)
  120. end
  121. # Adds target-level compile-time compiler options that aren't macro
  122. # definitions or include directories. Link-time options should be added via
  123. # lib_libraries.
  124. #
  125. # Args:
  126. # - type: PUBLIC, PRIVATE, or INTERFACE
  127. # - values: compiler flags, e.g. -fno-autolink
  128. def compile_options(type, *values)
  129. extra_command('target_compile_options', @name, type)
  130. .add_args(values)
  131. end
  132. # Adds target-level dependencies or link-time compiler options. CMake
  133. # interprets any quoted string that starts with "-" as an option and anything
  134. # else as a library target to depend upon.
  135. #
  136. # Args:
  137. # - type: PUBLIC, PRIVATE, or INTERFACE
  138. # - values: compiler flags, e.g. -fno-autolink
  139. def link_libraries(type, *dirs)
  140. extra_command('target_link_libraries', @name, type)
  141. .add_args(dirs)
  142. end
  143. private
  144. def extra_command(*key_args)
  145. key = key_args.join('|')
  146. command = @extras[key]
  147. if command.nil?
  148. command = CMakeCommand.new(*key_args)
  149. @extras[key] = command
  150. end
  151. return command
  152. end
  153. end
  154. # Generates a framework target based on podspec contents. Models the translation
  155. # of a single podspec (and possible subspecs) to a single CMake framework
  156. # target.
  157. class CMakeGenerator
  158. # Initializes the generator with the given root Pod::Spec and the binary
  159. # directory for the current CMake configuration.
  160. #
  161. # Args:
  162. # - spec: A root specification, the name of which becomes the name of the
  163. # Framework.
  164. # - path_list: A Pod::Sandbox::PathList used to cache file operations.
  165. # - cmake_binary_dir: A directory in which additional files may be written.
  166. def initialize(spec, path_list, cmake_binary_dir)
  167. @target = Framework.new(spec.name)
  168. headers_root = File.join(cmake_binary_dir, 'Headers')
  169. @headers_dir = File.join(headers_root, spec.name)
  170. @root = spec
  171. @target.set_property('FRAMEWORK', 'ON')
  172. @target.set_property('VERSION', spec.version)
  173. @target.include_directories('PRIVATE', headers_root, @headers_dir)
  174. @target.link_libraries('PUBLIC', "\"-framework Foundation\"")
  175. root_dir = Pathname.new(__FILE__).expand_path().dirname().dirname()
  176. @path_list = Pod::Sandbox::PathList.new(root_dir)
  177. end
  178. attr_reader :target
  179. # Adds information from the given Pod::Spec to the definition of the CMake
  180. # framework target. Subspecs are not automatically handled.
  181. #
  182. # Cocoapods subspecs are not independent libraries--they contribute sources
  183. # and dependencies to a final single Framework.
  184. #
  185. # Args:
  186. # - spec: A root or subspec that contributes to the final state of the of the
  187. # Framework.
  188. def add_framework(spec)
  189. spec = spec.consumer(PLATFORM)
  190. files = Pod::Sandbox::FileAccessor.new(@path_list, spec)
  191. sources = [
  192. files.source_files,
  193. files.public_headers,
  194. files.private_headers,
  195. ].flatten
  196. @target.add_sources(sources)
  197. add_headers(files, sources)
  198. add_dependencies(spec)
  199. add_framework_dependencies(spec)
  200. @target.compile_options('INTERFACE', '-F${CMAKE_CURRENT_BINARY_DIR}')
  201. @target.compile_options('PRIVATE', '${OBJC_FLAGS}')
  202. add_xcconfig('PRIVATE', spec.pod_target_xcconfig)
  203. add_xcconfig('PUBLIC', spec.user_target_xcconfig)
  204. end
  205. private
  206. # Sets up the framework headers so that compilation can succeed.
  207. # Xcode/CocoaPods allow for several different include mechanisms to work:
  208. #
  209. # * Unqualified headers, e.g. +#import "FIRLoggerLevel.h"+, typically
  210. # resolved via the header map.
  211. # * Qualified relative to some source root, e.g.
  212. # +#import "Public/FIRLoggerLevel.h"+, typically resolved by an include
  213. # path
  214. # * Framework imports, e.g. +#import <FirebaseCore/FIRLoggerLevel.h>+,
  215. # resolved by a build process that copies headers into the framework
  216. # structure.
  217. # * Umbrella imports e.g. +#import <FirebaseCore/FirebaseCore.h>+ (which
  218. # happens to import all the public headers).
  219. #
  220. # CMake's framework support is incomplete. It has no support at all for
  221. # generating umbrella headers.
  222. #
  223. # CMake also does not completely support framework imports. It does work for
  224. # sources outside the framework that want to build against it, but until the
  225. # framework has been completely built the headers aren't available in this
  226. # form. This prevents frameworks from referring to their own code via
  227. # framework imports.
  228. #
  229. # This method cheats by creating a subdirectory in the build results that has
  230. # symbolic links of all the public headers accessible with the right path.
  231. # This makes it possible to use framework imports within the framework itself.
  232. # The parent of this path is then added as a PRIVATE include directory of the
  233. # target, making it possible for the framework to see itself this way.
  234. def add_headers(files, sources)
  235. # CMake-built frameworks don't have a notion of private headers, but they
  236. # also don't have a notion of umbrella headers, so all framework headers
  237. # need to be accessed by name. This means that just dumping all the private
  238. # and public headers into what CMake considers the public headers makes
  239. # everything work as we expect.
  240. headers = [
  241. files.public_headers,
  242. files.private_headers
  243. ].flatten
  244. @target.add_public_headers(headers)
  245. # Also, link the headers into a directory that looks like a framework layout
  246. # so that self-references via framework imports work. These *must* be
  247. # symbolic links, otherwise our usual sloppiness causes file contents to be
  248. # included multiple times, usually resulting in ambiguity errors.
  249. FileUtils.mkdir_p(@headers_dir)
  250. headers.each do |header|
  251. FileUtils.ln_sf(header, File.join(@headers_dir, File.basename(header)))
  252. end
  253. # Simulate header maps by adding include paths for all the directories
  254. # containing non-public headers.
  255. hmap_dirs = Set.new()
  256. sources.each do |source|
  257. next if File.extname(source) != '.h'
  258. next if headers.include?(source)
  259. hmap_dirs.add(File.dirname(source))
  260. end
  261. @target.include_directories('PRIVATE', hmap_dirs.to_a.sort)
  262. end
  263. # Adds Pod::Spec +dependencies+ as target_link_libraries. Only root-specs are
  264. # added as dependencies because in the CMake build there can be only one
  265. # target for the framework.
  266. def add_dependencies(spec)
  267. prefix = "#{@root.name}/"
  268. spec.dependencies.each do |dep|
  269. # Dependencies on subspecs of this same spec are handled elsewhere.
  270. next if dep.name.start_with?(prefix)
  271. name = dep.name.sub(/\/.*/, '')
  272. @target.link_libraries('PUBLIC', name)
  273. end
  274. end
  275. # Adds target_link_libraries entries for all the items in the Pod::Spec
  276. # +frameworks+ attribute.
  277. def add_framework_dependencies(spec)
  278. spec.frameworks.each do |framework|
  279. @target.link_libraries('PUBLIC', "\"-framework #{framework}\"")
  280. end
  281. end
  282. # Mirrors known entries from the xcconfig entries into their equivalents in
  283. # CMake. This translates OTHER_CFLAGS, GCC_PREPROCESSOR_DEFINITIONS, and
  284. # HEADER_SEARCH_PATHS.
  285. #
  286. # Args:
  287. # - type: PUBLIC for +pod_user_xcconfig+ or PRIVATE for
  288. # +pod_target_xcconfig+.
  289. # - xcconfig: the hash of xcconfig values.
  290. def add_xcconfig(type, xcconfig)
  291. if xcconfig.empty?
  292. return
  293. end
  294. @target.compile_options(type, split(xcconfig['OTHER_CFLAGS']))
  295. defs = split(xcconfig['GCC_PREPROCESSOR_DEFINITIONS'])
  296. defs = defs.map { |x| '-D' + x }
  297. @target.compile_definitions(type, defs)
  298. @target.include_directories(type, split(xcconfig['HEADER_SEARCH_PATHS']))
  299. end
  300. # Splits a textual value in xcconfig. Always returns an array, but that array
  301. # may be empty if the value didn't exist in the podspec.
  302. def split(value)
  303. if value.nil?
  304. return []
  305. elsif value.kind_of?(String)
  306. return value.split
  307. else
  308. return [value]
  309. end
  310. end
  311. end
  312. # Processes a podspec file, translating all the specs within it into cmake file
  313. # describing how to build it.
  314. #
  315. # Args:
  316. # - podspec_file: The filename of the podspec to use as a source.
  317. # - cmake_file: The filename of the cmake script to produce.
  318. # - req_subspecs: Which subspecs to include. If empty, all subspecs are
  319. # included (which corresponds to CocoaPods behavior. The default_subspec
  320. # property is not handled.
  321. def process(podspec_file, cmake_file, *req_subspecs)
  322. root_dir = Pathname.new(__FILE__).expand_path().dirname().dirname()
  323. path_list = Pod::Sandbox::PathList.new(root_dir)
  324. spec = Pod::Spec.from_file(podspec_file)
  325. writer = Writer.new()
  326. writer.append <<~EOF
  327. # This file was generated by #{File.basename(__FILE__)}
  328. # from #{File.basename(podspec_file)}.
  329. # Do not edit!
  330. EOF
  331. cmake_binary_dir = File.expand_path(File.dirname(cmake_file))
  332. gen = CMakeGenerator.new(spec, path_list, cmake_binary_dir)
  333. gen.add_framework(spec)
  334. req_subspecs = normalize_requested_subspecs(spec, req_subspecs)
  335. req_subspecs = resolve_subspec_deps(spec, req_subspecs)
  336. spec.subspecs.each do |subspec|
  337. if req_subspecs.include?(subspec.name)
  338. gen.add_framework(subspec)
  339. end
  340. end
  341. gen.target.commands.each do |command|
  342. writer.write(command)
  343. end
  344. File.open(cmake_file, 'w') do |fd|
  345. fd.write(writer.result)
  346. end
  347. end
  348. # Translates the (possibly empty) list of requested subspecs into the list of
  349. # subspecs to actually include. If +req_subspecs+ is empty, returns all
  350. # subspecs. If non-empty, all subspecs are returned as qualified names, e.g.
  351. # "Logger" may become "GoogleUtilities/Logger".
  352. def normalize_requested_subspecs(spec, req_subspecs)
  353. subspecs = spec.subspecs
  354. if req_subspecs.empty?
  355. return subspecs.map { |s| s.name }
  356. else
  357. return req_subspecs.map do |name|
  358. if name.include?(?/)
  359. name
  360. else
  361. "#{spec.name}/#{name}"
  362. end
  363. end
  364. end
  365. end
  366. # Expands the list of requested subspecs to include any dependencies within the
  367. # same root subspec. For example, if +req_subspecs+ where
  368. #
  369. # +["GoogleUtilties/Logger"]+,
  370. #
  371. # the result would be
  372. #
  373. # +["GoogleUtilties/Logger", "GoogleUtilities/Environment"]+
  374. #
  375. # because Logger depends upon Environment within the same root spec.
  376. def resolve_subspec_deps(spec, req_subspecs)
  377. prefix = spec.name + '/'
  378. result = Set.new()
  379. while !req_subspecs.empty?
  380. req = req_subspecs.pop
  381. result.add(req)
  382. subspec = spec.subspec_by_name(req)
  383. subspec.dependencies(PLATFORM).each do |dep|
  384. if dep.name.start_with?(prefix) && !result.include?(dep.name)
  385. req_subspecs.push(dep.name)
  386. end
  387. end
  388. end
  389. return result.to_a.sort
  390. end
  391. # Writes CMake commands out to textual form, taking care of line wrapping.
  392. class Writer
  393. def initialize()
  394. @last_command = nil
  395. @result = ""
  396. end
  397. attr_reader :result
  398. def write(command)
  399. if command.skip?
  400. return
  401. end
  402. if command.name != @last_command
  403. @result << "\n"
  404. end
  405. @last_command = command.name
  406. single = "#{command.name}(#{command.rest.join(' ')})\n"
  407. if single.size < 80
  408. @result << single
  409. else
  410. @result << "#{command.name}(\n"
  411. command.rest.each do |arg|
  412. @result << " #{arg}\n"
  413. end
  414. @result << ")\n"
  415. end
  416. end
  417. def append(text)
  418. @result << text
  419. end
  420. end
  421. main(ARGV)