xref: /aosp_15_r20/external/skia/gn/gn_to_cmake.py (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1#!/usr/bin/env python3
2#
3# Copyright 2016 Google Inc.
4#
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8
9"""
10Usage: gn_to_cmake.py <json_file_name>
11
12gn gen out/config --ide=json --json-ide-script=../../gn/gn_to_cmake.py
13
14or
15
16gn gen out/config --ide=json
17python3 gn/gn_to_cmake.py out/config/project.json
18
19The first is recommended, as it will auto-update.
20"""
21
22
23import argparse
24import itertools
25import functools
26import json
27import posixpath
28import os
29import string
30import sys
31
32
33def CMakeStringEscape(a):
34  """Escapes the string 'a' for use inside a CMake string.
35
36  This means escaping
37  '\' otherwise it may be seen as modifying the next character
38  '"' otherwise it will end the string
39  ';' otherwise the string becomes a list
40
41  The following do not need to be escaped
42  '#' when the lexer is in string state, this does not start a comment
43  """
44  return a.replace('\\', '\\\\').replace(';', '\\;').replace('"', '\\"')
45
46
47def CMakeTargetEscape(a):
48  """Escapes the string 'a' for use as a CMake target name.
49
50  CMP0037 in CMake 3.0 restricts target names to "^[A-Za-z0-9_.:+-]+$"
51  The ':' is only allowed for imported targets.
52  """
53  def Escape(c):
54    if c in string.ascii_letters or c in string.digits or c in '_.+-':
55      return c
56    else:
57      return '__'
58  return ''.join(map(Escape, a))
59
60
61def SetVariable(out, variable_name, value):
62  """Sets a CMake variable."""
63  out.write('set("')
64  out.write(CMakeStringEscape(variable_name))
65  out.write('" "')
66  out.write(CMakeStringEscape(value))
67  out.write('")\n')
68
69
70def SetVariableList(out, variable_name, values):
71  """Sets a CMake variable to a list."""
72  if not values:
73    return SetVariable(out, variable_name, "")
74  if len(values) == 1:
75    return SetVariable(out, variable_name, values[0])
76  out.write('list(APPEND "')
77  out.write(CMakeStringEscape(variable_name))
78  out.write('"\n  "')
79  out.write('"\n  "'.join([CMakeStringEscape(value) for value in values]))
80  out.write('")\n')
81
82
83def SetFilesProperty(output, variable, property_name, values, sep):
84  """Given a set of source files, sets the given property on them."""
85  output.write('set_source_files_properties(')
86  WriteVariable(output, variable)
87  output.write(' PROPERTIES ')
88  output.write(property_name)
89  output.write(' "')
90  for value in values:
91    output.write(CMakeStringEscape(value))
92    output.write(sep)
93  output.write('")\n')
94
95
96def SetCurrentTargetProperty(out, property_name, values, sep=''):
97  """Given a target, sets the given property."""
98  out.write('set_target_properties("${target}" PROPERTIES ')
99  out.write(property_name)
100  out.write(' "')
101  for value in values:
102    out.write(CMakeStringEscape(value))
103    out.write(sep)
104  out.write('")\n')
105
106
107def CMakeGeneratorEscape(a):
108  """Escapes the string 'a' for use in a generator"""
109  return a.replace('>', '$<ANGLE-R>'
110         ).replace(',', '$<COMMA>'
111         ).replace(';', '$<SEMICOLON>')
112        #).replace('"', '$<QUOTE>') (Version 3.30.)
113
114
115def CMakeShellEscape(a):
116  """Escapes the string 'a' for separate_arguments(UNIX_COMMAND)."""
117  return a.replace('\\', '\\\\').replace(' ', '\\ ')
118
119
120def CurrentTargetShellCommand(out, command_name, values, lang):
121  """command_name("$target" PRIVATE "$<$<COMPILE_LANGUAGE:lang>:SHELL:values>")"""
122  if not values:
123    return
124  out.write(command_name)
125  out.write('("${target}" PRIVATE "')
126  # The following logically writes "$<condition:SHELL:values>" or "SHELL:values"
127  # The part in "" must be string escaped.
128  # The part in :> must be generator escaped.
129  # The part in SHELL: must be UNIX_COMMAND escaped.
130  if lang:
131    out.write('$<$<COMPILE_LANGUAGE:')
132    out.write(lang)
133    out.write('>:SHELL:')
134    for value in values:
135      out.write(CMakeStringEscape(CMakeGeneratorEscape(CMakeShellEscape(value))))
136      out.write(' ')
137    out.write('>')
138  else:
139    out.write('SHELL:')
140    for value in values:
141      out.write(CMakeStringEscape(CMakeShellEscape(value)))
142      out.write(' ')
143  out.write('")\n')
144
145
146def WriteVariable(output, variable_name, prepend=None):
147  if prepend:
148    output.write(prepend)
149  output.write('${')
150  output.write(variable_name)
151  output.write('}')
152
153
154# See GetSourceFileType in gn
155source_file_types = {
156  '.cc': 'cxx',
157  '.cpp': 'cxx',
158  '.cxx': 'cxx',
159  '.m': 'objc',
160  '.mm': 'objcc',
161  '.c': 'c',
162  '.s': 'asm',
163  '.S': 'asm',
164  '.asm': 'asm',
165  '.o': 'obj',
166  '.obj': 'obj',
167}
168
169
170class CMakeTargetType(object):
171  def __init__(self, command, modifier, property_modifier, is_linkable):
172    self.command = command
173    self.modifier = modifier
174    self.property_modifier = property_modifier
175    self.is_linkable = is_linkable
176CMakeTargetType.custom = CMakeTargetType('add_custom_target', 'SOURCES',
177                                         None, False)
178
179# See GetStringForOutputType in gn
180cmake_target_types = {
181  'unknown': CMakeTargetType.custom,
182  'group': CMakeTargetType('add_library', 'INTERFACE', None, True),
183  'executable': CMakeTargetType('add_executable', None, 'RUNTIME', True),
184  'loadable_module': CMakeTargetType('add_library', 'MODULE', 'LIBRARY', True),
185  'shared_library': CMakeTargetType('add_library', 'SHARED', 'LIBRARY', True),
186  'static_library': CMakeTargetType('add_library', 'STATIC', 'ARCHIVE', True),
187  'source_set': CMakeTargetType('add_library', 'OBJECT', None, False),
188  'copy': CMakeTargetType.custom,
189  'action': CMakeTargetType.custom,
190  'action_foreach': CMakeTargetType.custom,
191  'bundle_data': CMakeTargetType.custom,
192  'create_bundle': CMakeTargetType.custom,
193}
194
195
196def FindFirstOf(s, a):
197  return min(s.find(i) for i in a if i in s)
198
199
200class Project(object):
201  def __init__(self, project_json):
202    self.targets = project_json['targets']
203    build_settings = project_json['build_settings']
204    self.root_path = build_settings['root_path']
205    self.build_path = self.GetAbsolutePath(build_settings['build_dir'])
206
207  def GetAbsolutePath(self, path):
208    if path.startswith('//'):
209      return posixpath.join(self.root_path, path[2:])
210    else:
211      return path
212
213  def GetObjectSourceDependencies(self, gn_target_name, object_dependencies):
214    """All OBJECT libraries whose sources have not been absorbed."""
215    dependencies = self.targets[gn_target_name].get('deps', [])
216    for dependency in dependencies:
217      dependency_type = self.targets[dependency].get('type', None)
218      if dependency_type == 'source_set':
219        object_dependencies.add(dependency)
220      if dependency_type not in gn_target_types_that_absorb_objects:
221        self.GetObjectSourceDependencies(dependency, object_dependencies)
222
223  def GetObjectLibraryDependencies(self, gn_target_name, object_dependencies):
224    """All OBJECT libraries whose libraries have not been absorbed."""
225    dependencies = self.targets[gn_target_name].get('deps', [])
226    for dependency in dependencies:
227      dependency_type = self.targets[dependency].get('type', None)
228      if dependency_type == 'source_set':
229        object_dependencies.add(dependency)
230        self.GetObjectLibraryDependencies(dependency, object_dependencies)
231
232  def GetCMakeTargetName(self, gn_target_name):
233    # See <chromium>/src/tools/gn/label.cc#Resolve
234    # //base/test:test_support(//build/toolchain/win:msvc)
235    path_separator = FindFirstOf(gn_target_name, (':', '('))
236    location = None
237    name = None
238    toolchain = None
239    if not path_separator:
240      location = gn_target_name[2:]
241    else:
242      location = gn_target_name[2:path_separator]
243      toolchain_separator = gn_target_name.find('(', path_separator)
244      if toolchain_separator == -1:
245        name = gn_target_name[path_separator + 1:]
246      else:
247        if toolchain_separator > path_separator:
248          name = gn_target_name[path_separator + 1:toolchain_separator]
249        assert gn_target_name.endswith(')')
250        toolchain = gn_target_name[toolchain_separator + 1:-1]
251    assert location or name
252
253    cmake_target_name = None
254    if location.endswith('/' + name):
255      cmake_target_name = location
256    elif location:
257      cmake_target_name = location + '_' + name
258    else:
259      cmake_target_name = name
260    if toolchain:
261      cmake_target_name += '--' + toolchain
262    return CMakeTargetEscape(cmake_target_name)
263
264
265class Target(object):
266  def __init__(self, gn_target_name, project):
267    self.gn_name = gn_target_name
268    self.properties = project.targets[self.gn_name]
269    self.cmake_name = project.GetCMakeTargetName(self.gn_name)
270    self.gn_type = self.properties.get('type', None)
271    self.cmake_type = cmake_target_types.get(self.gn_type, None)
272
273
274def WriteAction(out, target, project, sources, synthetic_dependencies):
275  outputs = []
276  output_directories = set()
277  for output in target.properties.get('outputs', []):
278    output_abs_path = project.GetAbsolutePath(output)
279    outputs.append(output_abs_path)
280    output_directory = posixpath.dirname(output_abs_path)
281    if output_directory:
282      output_directories.add(output_directory)
283  outputs_name = '${target}__output'
284  SetVariableList(out, outputs_name, outputs)
285
286  out.write('add_custom_command(OUTPUT ')
287  WriteVariable(out, outputs_name)
288  out.write('\n')
289
290  if output_directories:
291    out.write('  COMMAND ${CMAKE_COMMAND} -E make_directory "')
292    out.write('" "'.join(map(CMakeStringEscape, output_directories)))
293    out.write('"\n')
294
295  script = target.properties['script']
296  arguments = target.properties['args']
297  out.write('  COMMAND python3 "')
298  out.write(CMakeStringEscape(project.GetAbsolutePath(script)))
299  out.write('"')
300  if arguments:
301    out.write('\n    "')
302    out.write('"\n    "'.join(map(CMakeStringEscape, arguments)))
303    out.write('"')
304  out.write('\n')
305
306  out.write('  DEPENDS ')
307  for sources_type_name in sources.values():
308    WriteVariable(out, sources_type_name, ' ')
309  out.write('\n')
310
311  #TODO: CMake 3.7 is introducing DEPFILE
312
313  out.write('  WORKING_DIRECTORY "')
314  out.write(CMakeStringEscape(project.build_path))
315  out.write('"\n')
316
317  out.write('  COMMENT "Action: ${target}"\n')
318
319  out.write('  VERBATIM)\n')
320
321  synthetic_dependencies.add(outputs_name)
322
323
324def ExpandPlaceholders(source, a):
325  source_dir, source_file_part = posixpath.split(source)
326  source_name_part, _ = posixpath.splitext(source_file_part)
327  #TODO: {{source_gen_dir}}, {{source_out_dir}}, {{response_file_name}}
328  return a.replace('{{source}}', source) \
329          .replace('{{source_file_part}}', source_file_part) \
330          .replace('{{source_name_part}}', source_name_part) \
331          .replace('{{source_dir}}', source_dir) \
332          .replace('{{source_root_relative_dir}}', source_dir)
333
334
335def WriteActionForEach(out, target, project, sources, synthetic_dependencies):
336  all_outputs = target.properties.get('outputs', [])
337  inputs = target.properties.get('sources', [])
338  # TODO: consider expanding 'output_patterns' instead.
339  outputs_per_input = int(len(all_outputs) / len(inputs))
340  for count, source in enumerate(inputs):
341    source_abs_path = project.GetAbsolutePath(source)
342
343    outputs = []
344    output_directories = set()
345    for output in all_outputs[outputs_per_input *  count:
346                              outputs_per_input * (count+1)]:
347      output_abs_path = project.GetAbsolutePath(output)
348      outputs.append(output_abs_path)
349      output_directory = posixpath.dirname(output_abs_path)
350      if output_directory:
351        output_directories.add(output_directory)
352    outputs_name = '${target}__output_' + str(count)
353    SetVariableList(out, outputs_name, outputs)
354
355    out.write('add_custom_command(OUTPUT ')
356    WriteVariable(out, outputs_name)
357    out.write('\n')
358
359    if output_directories:
360      out.write('  COMMAND ${CMAKE_COMMAND} -E make_directory "')
361      out.write('" "'.join(map(CMakeStringEscape, output_directories)))
362      out.write('"\n')
363
364    script = target.properties['script']
365    # TODO: need to expand {{xxx}} in arguments
366    arguments = target.properties['args']
367    out.write('  COMMAND python3 "')
368    out.write(CMakeStringEscape(project.GetAbsolutePath(script)))
369    out.write('"')
370    if arguments:
371      out.write('\n    "')
372      expand = functools.partial(ExpandPlaceholders, source_abs_path)
373      out.write('"\n    "'.join(map(CMakeStringEscape, map(expand,arguments))))
374      out.write('"')
375    out.write('\n')
376
377    out.write('  DEPENDS')
378    if 'input' in sources:
379      WriteVariable(out, sources['input'], ' ')
380    out.write(' "')
381    out.write(CMakeStringEscape(source_abs_path))
382    out.write('"\n')
383
384    #TODO: CMake 3.7 is introducing DEPFILE
385
386    out.write('  WORKING_DIRECTORY "')
387    out.write(CMakeStringEscape(project.build_path))
388    out.write('"\n')
389
390    out.write('  COMMENT "Action ${target} on ')
391    out.write(CMakeStringEscape(source_abs_path))
392    out.write('"\n')
393
394    out.write('  VERBATIM)\n')
395
396    synthetic_dependencies.add(outputs_name)
397
398
399def WriteCopy(out, target, project, sources, synthetic_dependencies):
400  inputs = target.properties.get('sources', [])
401  raw_outputs = target.properties.get('outputs', [])
402
403  # TODO: consider expanding 'output_patterns' instead.
404  outputs = []
405  for output in raw_outputs:
406    output_abs_path = project.GetAbsolutePath(output)
407    outputs.append(output_abs_path)
408  outputs_name = '${target}__output'
409  SetVariableList(out, outputs_name, outputs)
410
411  out.write('add_custom_command(OUTPUT ')
412  WriteVariable(out, outputs_name)
413  out.write('\n')
414
415  for src, dst in zip(inputs, outputs):
416    abs_src_path = CMakeStringEscape(project.GetAbsolutePath(src))
417    # CMake distinguishes between copying files and copying directories but
418    # gn does not. We assume if the src has a period in its name then it is
419    # a file and otherwise a directory.
420    if "." in os.path.basename(abs_src_path):
421      out.write('  COMMAND ${CMAKE_COMMAND} -E copy "')
422    else:
423      out.write('  COMMAND ${CMAKE_COMMAND} -E copy_directory "')
424    out.write(abs_src_path)
425    out.write('" "')
426    out.write(CMakeStringEscape(dst))
427    out.write('"\n')
428
429  out.write('  DEPENDS ')
430  for sources_type_name in sources.values():
431    WriteVariable(out, sources_type_name, ' ')
432  out.write('\n')
433
434  out.write('  WORKING_DIRECTORY "')
435  out.write(CMakeStringEscape(project.build_path))
436  out.write('"\n')
437
438  out.write('  COMMENT "Copy ${target}"\n')
439
440  out.write('  VERBATIM)\n')
441
442  synthetic_dependencies.add(outputs_name)
443
444
445def WriteCompilerFlags(out, target, project, sources):
446  # Hack, set linker language to c if no c or cxx files present.
447  # However, cannot set LINKER_LANGUAGE on INTERFACE (with source files) until 3.19.
448  if not 'c' in sources and not 'cxx' in sources and not target.cmake_type.modifier == "INTERFACE":
449    SetCurrentTargetProperty(out, 'LINKER_LANGUAGE', ['C'])
450
451  # Mark uncompiled sources as uncompiled.
452  if 'input' in sources:
453    SetFilesProperty(out, sources['input'], 'HEADER_FILE_ONLY', ('True',), '')
454  if 'other' in sources:
455    SetFilesProperty(out, sources['other'], 'HEADER_FILE_ONLY', ('True',), '')
456
457  # Mark object sources as linkable.
458  if 'obj' in sources:
459    SetFilesProperty(out, sources['obj'], 'EXTERNAL_OBJECT', ('True',), '')
460
461  # TODO: 'output_name', 'output_dir', 'output_extension'
462  # This includes using 'source_outputs' to direct compiler output.
463
464  # Includes
465  includes = target.properties.get('include_dirs', [])
466  if includes:
467    out.write('set_property(TARGET "${target}" ')
468    out.write('APPEND PROPERTY INCLUDE_DIRECTORIES')
469    for include_dir in includes:
470      out.write('\n  "')
471      out.write(project.GetAbsolutePath(include_dir))
472      out.write('"')
473    out.write(')\n')
474
475  # Defines
476  defines = target.properties.get('defines', [])
477  if defines:
478    SetCurrentTargetProperty(out, 'COMPILE_DEFINITIONS', defines, ';')
479
480  # Compile flags
481  def WriteCompileOptions(out, source_types, prop, lang):
482    if any(k in sources for k in source_types):
483      cflags = target.properties.get(prop, [])
484      CurrentTargetShellCommand(out, 'target_compile_options', cflags, lang)
485
486  WriteCompileOptions(out, ('asm',), 'asmflags', 'ASM')
487  WriteCompileOptions(out, ('c', 'cxx', 'objc', 'objcc'), 'cflags', 'C,CXX,OBJC,OBJCXX')
488  WriteCompileOptions(out, ('c', 'objc'), 'cflags_c', 'C,OBJC')
489  WriteCompileOptions(out, ('cxx', 'objcc'), 'cflags_cc', 'CXX,OBJCXX')
490  WriteCompileOptions(out, ('objc',), 'cflags_objc', 'OBJC')
491  WriteCompileOptions(out, ('objcc',), 'cflags_objcc', 'OBJCXX')
492
493  # Linker flags
494  ldflags = target.properties.get('ldflags', [])
495  if ldflags:
496    CurrentTargetShellCommand(out, 'target_link_options', ldflags, None)
497
498
499gn_target_types_that_absorb_objects = (
500  'executable',
501  'loadable_module',
502  'shared_library',
503  'static_library'
504)
505
506
507def WriteSourceVariables(out, target, project):
508  # gn separates the sheep from the goats based on file extensions.
509  # A full separation is done here because of flag handing (see Compile flags).
510  source_types = {'cxx':[], 'c':[], 'asm':[], 'objc':[], 'objcc':[],
511                  'obj':[], 'obj_target':[], 'input':[], 'other':[]}
512
513  all_sources = target.properties.get('sources', [])
514
515  # As of cmake 3.11 add_library must have sources.
516  # If there are no sources, add empty.cpp as the file to compile.
517  # Unless it's an INTERFACE, which must not have sources until 3.19.
518  if len(all_sources) == 0 and not target.cmake_type.modifier == "INTERFACE":
519    all_sources.append(posixpath.join(project.build_path, 'empty.cpp'))
520
521  # TODO .def files on Windows
522  for source in all_sources:
523    _, ext = posixpath.splitext(source)
524    source_abs_path = project.GetAbsolutePath(source)
525    source_types[source_file_types.get(ext, 'other')].append(source_abs_path)
526
527  for input_path in target.properties.get('inputs', []):
528    input_abs_path = project.GetAbsolutePath(input_path)
529    source_types['input'].append(input_abs_path)
530
531  # OBJECT library dependencies need to be listed as sources.
532  # Only executables and non-OBJECT libraries may reference an OBJECT library.
533  # https://gitlab.kitware.com/cmake/cmake/issues/14778
534  if target.gn_type in gn_target_types_that_absorb_objects:
535    object_dependencies = set()
536    project.GetObjectSourceDependencies(target.gn_name, object_dependencies)
537    for dependency in object_dependencies:
538      cmake_dependency_name = project.GetCMakeTargetName(dependency)
539      obj_target_sources = '$<TARGET_OBJECTS:' + cmake_dependency_name + '>'
540      source_types['obj_target'].append(obj_target_sources)
541
542  sources = {}
543  for source_type, sources_of_type in source_types.items():
544    if sources_of_type:
545      sources[source_type] = '${target}__' + source_type + '_srcs'
546      SetVariableList(out, sources[source_type], sources_of_type)
547  return sources
548
549
550def WriteTarget(out, target, project):
551  out.write('\n#')
552  out.write(target.gn_name)
553  out.write('\n')
554
555  if target.cmake_type is None:
556    print ('Target %s has unknown target type %s, skipping.' %
557          (        target.gn_name,            target.gn_type ) )
558    return
559
560  SetVariable(out, 'target', target.cmake_name)
561
562  sources = WriteSourceVariables(out, target, project)
563
564  synthetic_dependencies = set()
565  if target.gn_type == 'action':
566    WriteAction(out, target, project, sources, synthetic_dependencies)
567  if target.gn_type == 'action_foreach':
568    WriteActionForEach(out, target, project, sources, synthetic_dependencies)
569  if target.gn_type == 'copy':
570    WriteCopy(out, target, project, sources, synthetic_dependencies)
571
572  out.write(target.cmake_type.command)
573  out.write('("${target}"')
574  if target.cmake_type.modifier is not None:
575    out.write(' ')
576    out.write(target.cmake_type.modifier)
577  for sources_type_name in sources.values():
578    WriteVariable(out, sources_type_name, ' ')
579  if synthetic_dependencies:
580    out.write(' DEPENDS')
581    for synthetic_dependencie in synthetic_dependencies:
582      WriteVariable(out, synthetic_dependencie, ' ')
583  out.write(')\n')
584
585  if target.cmake_type.command != 'add_custom_target':
586    WriteCompilerFlags(out, target, project, sources)
587
588  libraries = set()
589  nonlibraries = set()
590
591  dependencies = set(target.properties.get('deps', []))
592  # Transitive OBJECT libraries are in sources.
593  # Those sources are dependent on the OBJECT library dependencies.
594  # Those sources cannot bring in library dependencies.
595  object_dependencies = set()
596  if target.gn_type != 'source_set':
597    project.GetObjectLibraryDependencies(target.gn_name, object_dependencies)
598  for object_dependency in object_dependencies:
599    dependencies.update(project.targets.get(object_dependency).get('deps', []))
600
601  for dependency in dependencies:
602    gn_dependency_type = project.targets.get(dependency, {}).get('type', None)
603    cmake_dependency_type = cmake_target_types.get(gn_dependency_type, None)
604    cmake_dependency_name = project.GetCMakeTargetName(dependency)
605    if cmake_dependency_type.command != 'add_library':
606      nonlibraries.add(cmake_dependency_name)
607    elif cmake_dependency_type.modifier != 'OBJECT':
608      if target.cmake_type.is_linkable:
609        libraries.add(cmake_dependency_name)
610      else:
611        nonlibraries.add(cmake_dependency_name)
612
613  # Non-library dependencies.
614  if nonlibraries:
615    out.write('add_dependencies("${target}"')
616    for nonlibrary in nonlibraries:
617      out.write('\n  "')
618      out.write(nonlibrary)
619      out.write('"')
620    out.write(')\n')
621
622  # Non-OBJECT library dependencies.
623  combined_library_lists = [target.properties.get(key, []) for key in ['libs', 'frameworks']]
624  external_libraries = list(itertools.chain(*combined_library_lists))
625  if target.cmake_type.is_linkable and (external_libraries or libraries):
626    library_dirs = target.properties.get('lib_dirs', [])
627    if library_dirs:
628      SetVariableList(out, '${target}__library_directories', library_dirs)
629
630    system_libraries = []
631    for external_library in external_libraries:
632      if '/' in external_library:
633        libraries.add(project.GetAbsolutePath(external_library))
634      else:
635        if external_library.endswith('.framework'):
636          external_library = external_library[:-len('.framework')]
637        system_library = 'library__' + external_library
638        if library_dirs:
639          system_library = system_library + '__for_${target}'
640        out.write('find_library("')
641        out.write(CMakeStringEscape(system_library))
642        out.write('" "')
643        out.write(CMakeStringEscape(external_library))
644        out.write('"')
645        if library_dirs:
646          out.write(' PATHS "')
647          WriteVariable(out, '${target}__library_directories')
648          out.write('"')
649        out.write(')\n')
650        system_libraries.append(system_library)
651    out.write('target_link_libraries("${target}"')
652    if (target.cmake_type.modifier == "INTERFACE"):
653      out.write(' INTERFACE')
654    for library in libraries:
655      out.write('\n  "')
656      out.write(CMakeStringEscape(library))
657      out.write('"')
658    for system_library in system_libraries:
659      WriteVariable(out, system_library, '\n  "')
660      out.write('"')
661    out.write(')\n')
662
663
664def WriteProject(project, ninja_executable):
665  out = open(posixpath.join(project.build_path, 'CMakeLists.txt'), 'w+')
666  extName = posixpath.join(project.build_path, 'CMakeLists.ext')
667  out.write('# Generated by gn_to_cmake.py.\n')
668  out.write('cmake_minimum_required(VERSION 3.16 FATAL_ERROR)\n')
669  out.write('cmake_policy(VERSION 3.16)\n')
670  out.write('project(Skia LANGUAGES C CXX)\n\n')
671
672  out.write('file(WRITE "')
673  out.write(CMakeStringEscape(posixpath.join(project.build_path, "empty.cpp")))
674  out.write('")\n')
675
676  # Update the gn generated ninja build.
677  # If a build file has changed, this will update CMakeLists.ext if
678  # gn gen out/config --ide=json --json-ide-script=../../gn/gn_to_cmake.py
679  # style was used to create this config.
680  out.write('execute_process(COMMAND\n  "')
681  out.write(CMakeStringEscape(ninja_executable))
682  out.write('" -C "')
683  out.write(CMakeStringEscape(project.build_path))
684  out.write('" build.ninja\n')
685  out.write('  RESULT_VARIABLE ninja_result)\n')
686  out.write('if (ninja_result)\n')
687  out.write('  message(WARNING ')
688  out.write('"Regeneration failed running ninja: ${ninja_result}")\n')
689  out.write('endif()\n')
690
691  out.write('include("')
692  out.write(CMakeStringEscape(extName))
693  out.write('")\n')
694  # This lets Clion find the emscripten header files when working with CanvasKit.
695  out.write('include_directories(SYSTEM "$ENV{EMSDK}/upstream/emscripten/system/include/")\n')
696  out.close()
697
698  out = open(extName, 'w+')
699  out.write('# Generated by gn_to_cmake.py.\n')
700  out.write('cmake_minimum_required(VERSION 3.16 FATAL_ERROR)\n')
701  out.write('cmake_policy(VERSION 3.16)\n\n')
702
703  # OBJC and OBJCXX need to be enabled manually.
704  out.write('if (APPLE)\n')
705  out.write('  enable_language(OBJC OBJCXX)\n')
706  out.write('endif()\n')
707
708  # enable ASM last
709  out.write('enable_language(ASM)\n\n')
710  # ASM-ATT does not support .S files.
711  # output.write('enable_language(ASM-ATT)\n')
712
713  # Current issues with automatic re-generation:
714  # The gn generated build.ninja target uses build.ninja.d
715  #   but build.ninja.d does not contain the ide or gn.
716  # Currently the ide is not run if the project.json file is not changed
717  #   but the ide needs to be run anyway if it has itself changed.
718  #   This can be worked around by deleting the project.json file.
719  out.write('file(READ "')
720  gn_deps_file = posixpath.join(project.build_path, 'build.ninja.d')
721  out.write(CMakeStringEscape(gn_deps_file))
722  out.write('" "gn_deps_file_content")\n')
723
724  out.write('string(REGEX REPLACE "^[^:]*: " "" ')
725  out.write('gn_deps_string ${gn_deps_file_content})\n')
726
727  # One would think this would need to worry about escaped spaces
728  # but gn doesn't escape spaces here (it generates invalid .d files).
729  out.write('string(REPLACE " " ";" "gn_deps" ${gn_deps_string})\n')
730  out.write('foreach("gn_dep" ${gn_deps})\n')
731  out.write('  configure_file("')
732  out.write(CMakeStringEscape(project.build_path))
733  out.write('${gn_dep}" "CMakeLists.devnull" COPYONLY)\n')
734  out.write('endforeach("gn_dep")\n')
735
736  out.write('list(APPEND other_deps "')
737  out.write(CMakeStringEscape(os.path.abspath(__file__)))
738  out.write('")\n')
739  out.write('foreach("other_dep" ${other_deps})\n')
740  out.write('  configure_file("${other_dep}" "CMakeLists.devnull" COPYONLY)\n')
741  out.write('endforeach("other_dep")\n')
742
743  for target_name in project.targets.keys():
744    out.write('\n')
745    WriteTarget(out, Target(target_name, project), project)
746
747
748def main():
749  parser = argparse.ArgumentParser()
750  # gn passes the json file as the first parameter, see --json-file-name
751  parser.add_argument("json_path", help="Path to --ide=json file")
752  # Extra optional args, see --json-ide-script-args
753  parser.add_argument("--ninja-executable", default="ninja", help="Path to ninja")
754  args = parser.parse_args()
755
756  json_path = args.json_path
757  project = None
758  with open(json_path, 'r') as json_file:
759    project = json.loads(json_file.read())
760
761  WriteProject(Project(project), args.ninja_executable)
762
763
764if __name__ == "__main__":
765  main()
766