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