• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import argparse
6import json
7import os
8import subprocess
9import sys
10import tempfile
11
12
13def fix_module_imports(header_path, output_path):
14  """Convert modules import to work without -fmodules support.
15
16  The Swift compiler assumes that the generated Objective-C header will be
17  imported from code compiled with module support enabled (-fmodules). The
18  generated code thus uses @import and provides no fallback if modules are
19  not enabled.
20
21  This function converts the generated header to instead use #import. It
22  assumes that `@import Foo;` can be replaced by `#import <Foo/Foo.h>`.
23
24  The header is read at `header_path` and written to `output_path`.
25  """
26
27  header_contents = []
28  with open(header_path, 'r') as header_file:
29    for line in header_file:
30      if line == '#if __has_feature(objc_modules)\n':
31        header_contents.append('#if 1  // #if __has_feature(objc_modules)\n')
32        nesting_level = 1
33        for line in header_file:
34          if line == '#endif\n':
35            nesting_level -= 1
36          elif line.startswith('@import'):
37            name = line.split()[1].split(';')[0]
38            if name != 'ObjectiveC':
39              header_contents.append(f'#import <{name}/{name}.h>  ')
40            header_contents.append('// ')
41          elif line.startswith('#if'):
42            nesting_level += 1
43
44          header_contents.append(line)
45          if nesting_level == 0:
46            break
47      else:
48        header_contents.append(line)
49
50  with open(output_path, 'w') as header_file:
51    for line in header_contents:
52      header_file.write(line)
53
54
55def compile_module(module, sources, settings, extras, tmpdir):
56  """Compile `module` from `sources` using `settings`."""
57  output_file_map = {}
58  if settings.whole_module_optimization:
59    output_file_map[''] = {
60        'object': os.path.join(settings.object_dir, module + '.o'),
61        'dependencies': os.path.join(tmpdir, module + '.d'),
62    }
63  else:
64    for source in sources:
65      name, _ = os.path.splitext(os.path.basename(source))
66      output_file_map[source] = {
67          'object': os.path.join(settings.object_dir, name + '.o'),
68          'dependencies': os.path.join(tmpdir, name + '.d'),
69      }
70
71  for key in ('module_path', 'header_path', 'depfile'):
72    path = getattr(settings, key)
73    if os.path.exists(path):
74      os.unlink(path)
75    if key == 'module_path':
76      for ext in '.swiftdoc', '.swiftsourceinfo':
77        path = os.path.splitext(getattr(settings, key))[0] + ext
78        if os.path.exists(path):
79          os.unlink(path)
80    directory = os.path.dirname(path)
81    if not os.path.exists(directory):
82      os.makedirs(directory)
83
84  if not os.path.exists(settings.object_dir):
85    os.makedirs(settings.object_dir)
86
87  if not os.path.exists(settings.pch_output_dir):
88    os.makedirs(settings.pch_output_dir)
89
90  for key in output_file_map:
91    path = output_file_map[key]['object']
92    if os.path.exists(path):
93      os.unlink(path)
94
95  output_file_map.setdefault('', {})['swift-dependencies'] = \
96      os.path.join(tmpdir, module + '.swift.d')
97
98  output_file_map_path = os.path.join(tmpdir, module + '.json')
99  with open(output_file_map_path, 'w') as output_file_map_file:
100    output_file_map_file.write(json.dumps(output_file_map))
101    output_file_map_file.flush()
102
103  extra_args = []
104  if settings.file_compilation_dir:
105    extra_args.extend([
106        '-file-compilation-dir',
107        settings.file_compilation_dir,
108    ])
109
110  if settings.bridge_header:
111    extra_args.extend([
112        '-import-objc-header',
113        os.path.abspath(settings.bridge_header),
114    ])
115
116  if settings.whole_module_optimization:
117    extra_args.append('-whole-module-optimization')
118
119  if settings.target:
120    extra_args.extend([
121        '-target',
122        settings.target,
123    ])
124
125  if settings.sdk:
126    extra_args.extend([
127        '-sdk',
128        os.path.abspath(settings.sdk),
129    ])
130
131  if settings.swift_version:
132    extra_args.extend([
133        '-swift-version',
134        settings.swift_version,
135    ])
136
137  if settings.include_dirs:
138    for include_dir in settings.include_dirs:
139      extra_args.append('-I' + include_dir)
140
141  if settings.system_include_dirs:
142    for system_include_dir in settings.system_include_dirs:
143      extra_args.extend(['-Xcc', '-isystem', '-Xcc', system_include_dir])
144
145  if settings.framework_dirs:
146    for framework_dir in settings.framework_dirs:
147      extra_args.extend([
148          '-F',
149          framework_dir,
150      ])
151
152  if settings.system_framework_dirs:
153    for system_framework_dir in settings.system_framework_dirs:
154      extra_args.extend([
155          '-F',
156          system_framework_dir,
157      ])
158
159  if settings.enable_cxx_interop:
160    extra_args.extend([
161        '-Xfrontend',
162        '-enable-cxx-interop',
163    ])
164
165  # The swiftc compiler uses a global module cache that is not robust against
166  # changes in the sub-modules nor against corruption (see crbug.com/1358073).
167  # Force the compiler to store the module cache in a sub-directory of `tmpdir`
168  # to ensure a pristine module cache is used for every compiler invocation.
169  module_cache_path = os.path.join(tmpdir, settings.swiftc_version,
170                                   'ModuleCache')
171
172  # If the generated header is post-processed, generate it to a temporary
173  # location (to avoid having the file appear to suddenly change).
174  if settings.fix_module_imports:
175    header_path = os.path.join(tmpdir, f'{module}.h')
176  else:
177    header_path = settings.header_path
178
179  process = subprocess.Popen([
180      settings.swift_toolchain_path + '/usr/bin/swiftc',
181      '-parse-as-library',
182      '-module-name',
183      module,
184      '-module-cache-path',
185      module_cache_path,
186      '-emit-object',
187      '-emit-dependencies',
188      '-emit-module',
189      '-emit-module-path',
190      settings.module_path,
191      '-emit-objc-header',
192      '-emit-objc-header-path',
193      header_path,
194      '-output-file-map',
195      output_file_map_path,
196      '-pch-output-dir',
197      os.path.abspath(settings.pch_output_dir),
198  ] + extra_args + extras + sources)
199
200  process.communicate()
201  if process.returncode:
202    sys.exit(process.returncode)
203
204  if settings.fix_module_imports:
205    fix_module_imports(header_path, settings.header_path)
206
207  # The swiftc compiler generates depfile that uses absolute paths, but
208  # ninja requires paths in depfiles to be identical to paths used in
209  # the build.ninja files.
210  #
211  # Since gn generates paths relative to the build directory for all paths
212  # below the repository checkout, we need to convert those to relative
213  # paths.
214  #
215  # See https://crbug.com/1287114 for build failure that happen when the
216  # paths in the depfile are kept absolute.
217  out_dir = os.getcwd() + os.path.sep
218  src_dir = os.path.abspath(settings.root_dir) + os.path.sep
219
220  depfile_content = dict()
221  for key in output_file_map:
222
223    # When whole module optimisation is disabled, there will be an entry
224    # with an empty string as the key and only ('swift-dependencies') as
225    # keys in the value dictionary. This is expected, so skip entry that
226    # do not include 'dependencies' in their keys.
227    depencency_file_path = output_file_map[key].get('dependencies')
228    if not depencency_file_path:
229      continue
230
231    for line in open(depencency_file_path):
232      output, inputs = line.split(' : ', 2)
233      _, ext = os.path.splitext(output)
234      if ext == '.o':
235        key = output
236      else:
237        key = os.path.splitext(settings.module_path)[0] + ext
238      if key not in depfile_content:
239        depfile_content[key] = set()
240      for path in inputs.split():
241        if path.startswith(src_dir) or path.startswith(out_dir):
242          path = os.path.relpath(path, out_dir)
243        depfile_content[key].add(path)
244
245  with open(settings.depfile, 'w') as depfile:
246    keys = sorted(depfile_content.keys())
247    for key in sorted(keys):
248      depfile.write('%s : %s\n' % (key, ' '.join(sorted(depfile_content[key]))))
249
250
251def main(args):
252  parser = argparse.ArgumentParser(add_help=False)
253  parser.add_argument('-module-name', help='name of the Swift module')
254  parser.add_argument('-include',
255                      '-I',
256                      action='append',
257                      dest='include_dirs',
258                      help='add directory to header search path')
259  parser.add_argument('-isystem',
260                      action='append',
261                      dest='system_include_dirs',
262                      help='add directory to system header search path')
263  parser.add_argument('sources', nargs='+', help='Swift source file to compile')
264  parser.add_argument('-whole-module-optimization',
265                      action='store_true',
266                      help='enable whole module optimization')
267  parser.add_argument('-object-dir',
268                      help='path to the generated object files directory')
269  parser.add_argument('-pch-output-dir',
270                      help='path to directory where .pch files are saved')
271  parser.add_argument('-module-path', help='path to the generated module file')
272  parser.add_argument('-header-path', help='path to the generated header file')
273  parser.add_argument('-bridge-header',
274                      help='path to the Objective-C bridge header')
275  parser.add_argument('-depfile', help='path to the generated depfile')
276  parser.add_argument('-swift-version',
277                      help='version of Swift language to support')
278  parser.add_argument('-target',
279                      action='store',
280                      help='generate code for the given target <triple>')
281  parser.add_argument('-sdk', action='store', help='compile against sdk')
282  parser.add_argument('-F',
283                      dest='framework_dirs',
284                      action='append',
285                      help='add dir to framework search path')
286  parser.add_argument('-Fsystem',
287                      '-iframework',
288                      dest='system_framework_dirs',
289                      action='append',
290                      help='add dir to system framework search path')
291  parser.add_argument('-root-dir',
292                      dest='root_dir',
293                      action='store',
294                      required=True,
295                      help='path to the root of the repository')
296  parser.add_argument('-swift-toolchain-path',
297                      default='',
298                      action='store',
299                      dest='swift_toolchain_path',
300                      help='path to the root of the Swift toolchain')
301  parser.add_argument('-file-compilation-dir',
302                      default='',
303                      action='store',
304                      help='compilation directory to embed in the debug info')
305  parser.add_argument('-enable-cxx-interop',
306                      dest='enable_cxx_interop',
307                      action='store_true',
308                      help='allow importing C++ modules into Swift')
309  parser.add_argument('-fix-module-imports',
310                      action='store_true',
311                      help='enable hack to fix module imports')
312  parser.add_argument('-swiftc-version',
313                      default='',
314                      action='store',
315                      help='version of swiftc compiler')
316  parser.add_argument('-xcode-version',
317                      default='',
318                      action='store',
319                      help='version of xcode')
320
321  parsed, extras = parser.parse_known_args(args)
322  with tempfile.TemporaryDirectory() as tmpdir:
323    compile_module(parsed.module_name, parsed.sources, parsed, extras, tmpdir)
324
325
326if __name__ == '__main__':
327  sys.exit(main(sys.argv[1:]))
328