Reland "Reland "[ios] Improve build/toolchain/ios/swiftc.py script""

This is a reland of commit e628fca793dea9493483753d202c5ca54a52cb9d

With https://gn-review.googlesource.com/16860, the ninja build action
that invokes the swiftc.py script has `restat = 1`. This causes ninja
to support the optimisation that output are not changed if not needed
(e.g. if input was just reformated, the compilation was not required).

This ensures that "no-op" builds are not considered dirty, and allow
to re-enable the FileWriter optimisation (to allow swiftc to see that
the compilation is not needed either, when possible).

Checked locally that this fixes the "no-op" build considered dirty
failure that was reproducible at 100% rate with previous version of
gn (without `restat = 1`).

Original change's description:
> Reland "[ios] Improve build/toolchain/ios/swiftc.py script"
>
> This is a reland of commit 555b7f43f59a6291b935c1dd8e99fc6ce77d183e
>
> Fixes the following issues that lead to reverting the original CL:
>
> 1.  The compilation was sometimes considered dirty after an incremental
>     build due to optimizations in both the script (FileWriter) and in
>     swiftc itself where output file are not overwritten if identical.
>
>     This was fixed by removing the use of FileWriter from the script and
>     by touching all the outputs of the compilation (thus the file should
>     now have newer timestamp than the source).
>
> 2.  The compilation for catalyst environment was broken because swiftc
>     expects different command-line argument for that environment (i.e.
>     -isystem is unsupported, -Xcc ... is also unsupported).
>
>     The script is modified to check whether the target triple
>     corresponds to the catalyst environment (i.e. matches *-macabi)
>     and constructs an acceptable command-line.
>
> 3.  As `gn check ...` did not know about the Objective-C generated
>     header for Swift modules, some dependencies were missing which
>     also caused dirty build after an incremental build. This was
>     fixed by https://crrev.com/c/5348787.
>
> Original change's description:
> > [ios] Improve build/toolchain/ios/swiftc.py script
> >
> > Change the script build/toolchain/ios/swiftc.py to configure the
> > swift compiler to generate $input.swiftconstvalues files (needed
> > for Intents API in swift).
> >
> > Improve the code to put all the derived data in one directory,
> > which allow removing the need for -pch-output-dir $dir from the
> > invocation of the script and its list of outputs (should avoid
> > the compilation error where $dir is created as a file instead of
> > a directory).
> >
> > Change toolchain.gni to pass all the tool versions as environment
> > variables instead of command-line arguments (reduce the length of
> > the command-line and remove the need to parse those arguments in
> > the script).
> >
> > Allow keeping the derived data dir between invocations of the
> > script (which may improve the build performance as the swiftc
> > binary will not have to rebuild the module cache each time).
> >
> > Bug: 326059372
> > Change-Id: I5e42676eb917fe2d355383aaa39a60d5674816fb
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5319176
> > Commit-Queue: Sylvain Defresne <sdefresne@chromium.org>
> > Reviewed-by: Federica Germinario <fedegermi@google.com>
> > Cr-Commit-Position: refs/heads/main@{#1268444}
>
> Bug: 326059372
> Cq-Include-Trybots: luci.chromium.try:ios-catalyst
> Change-Id: I94b9783441b2f92dc3e6caf8bb6a3522bc0cb419
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5349118
> Reviewed-by: Federica Germinario <fedegermi@google.com>
> Commit-Queue: Sylvain Defresne <sdefresne@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1269482}

Bug: 326059372
Change-Id: Ibb2d8aa5b169ec4f0fb2c5b413ba5b54eb50dde1
Cq-Include-Trybots: luci.chromium.try:ios-catalyst
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5352649
Reviewed-by: Federica Germinario <fedegermi@google.com>
Commit-Queue: Sylvain Defresne <sdefresne@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1272108}
diff --git a/build/toolchain/apple/toolchain.gni b/build/toolchain/apple/toolchain.gni
index 8fd4293..37360d2 100644
--- a/build/toolchain/apple/toolchain.gni
+++ b/build/toolchain/apple/toolchain.gni
@@ -36,6 +36,11 @@
   # for official builds, and "false" for all other builds.
   swift_whole_module_optimization = -1
 
+  # If true, the intermediate build products of swift module compilation will
+  # be kept after the invocation of the swiftc compiler. Otherwise they will
+  # deleted between each invocation.
+  swift_keep_intermediate_files = false
+
   # If unspecified, will use the toolchain downloaded via deps.
   swift_toolchain_path = -1
 }
@@ -76,16 +81,17 @@
 # but it will cause edges to be marked as dirty if the ninja files are
 # regenerated. See https://crbug.com/619083 for details. A proper fix
 # would be to have inputs to tools (https://crbug.com/621119).
-tool_versions =
-    exec_script("get_tool_mtime.py",
-                rebase_path([
-                              "//build/toolchain/apple/filter_libtool.py",
-                              "//build/toolchain/apple/linker_driver.py",
-                              "//build/toolchain/ios/compile_xcassets.py",
-                              "//build/toolchain/ios/swiftc.py",
-                            ],
-                            root_build_dir),
-                "trim scope")
+tool_versions = exec_script(
+        "get_tool_mtime.py",
+        rebase_path([
+                      "//build/toolchain/apple/filter_libtool.py",
+                      "//build/toolchain/apple/linker_driver.py",
+                      "//build/toolchain/ios/compile_xcassets.py",
+                      "//build/toolchain/ios/const_extract_protocols.json",
+                      "//build/toolchain/ios/swiftc.py",
+                    ],
+                    root_build_dir),
+        "trim scope")
 
 # Shared toolchain definition. Invocations should set current_os to set the
 # build args in this definition. This is titled "single_apple_toolchain"
@@ -645,73 +651,64 @@
         depfile = "{{target_out_dir}}/{{module_name}}.d"
         depsformat = "gcc"
 
-        outputs = [
-          # The module needs to be the first output listed. The blank line after
-          # the module is required to prevent `gn format` from changing the file
-          # order.
-          "{{target_gen_dir}}/{{module_name}}.swiftmodule",
+        _header_path = "{{target_gen_dir}}/{{target_output_name}}.h"
+        _output_dir = "{{target_out_dir}}/{{label_name}}"
 
-          "{{target_gen_dir}}/{{target_output_name}}.h",
-          "{{target_gen_dir}}/{{module_name}}.swiftdoc",
-          "{{target_gen_dir}}/{{module_name}}.swiftsourceinfo",
+        outputs = [
+          _header_path,
+          "$_output_dir/{{module_name}}-OutputFileMap.json",
+          "$_output_dir/{{module_name}}.SwiftFileList",
+          "$_output_dir/{{module_name}}.abi.json",
+          "$_output_dir/{{module_name}}.d",
+          "$_output_dir/{{module_name}}.dia",
+          "$_output_dir/{{module_name}}.swiftdoc",
+          "$_output_dir/{{module_name}}.swiftmodule",
+          "$_output_dir/{{module_name}}.swiftsourceinfo",
         ]
 
+        partial_outputs = [ "$_output_dir/{{source_name_part}}.o" ]
+
+        # The list of outputs and partial_outputs change whether the whole
+        # module optimization is enabled or not.
+        if (swift_whole_module_optimization) {
+          outputs += [
+            "$_output_dir/{{module_name}}.swiftconstvalues",
+            "$_output_dir/{{module_name}}.swiftdeps",
+          ]
+        } else {
+          outputs += [ "$_output_dir/{{module_name}}.priors" ]
+          partial_outputs += [
+            "$_output_dir/{{source_name_part}}.d",
+            "$_output_dir/{{source_name_part}}.dia",
+            "$_output_dir/{{source_name_part}}.swiftdeps",
+            "$_output_dir/{{source_name_part}}.swiftconstvalues",
+          ]
+        }
+
+        # If configured to keep the intermediate build files, pass the flag
+        # to the script and inform gn of the stamp file only (as the other
+        # files have names that cannot be predicted without invoking swiftc).
+        if (swift_keep_intermediate_files) {
+          _derived_data_dir = "$_output_dir/DerivedData"
+          outputs += [ "$_derived_data_dir/{{module_name}}.stamp" ]
+        }
+
         # Additional flags passed to the wrapper script but that are only
         # set conditionally.
         _extra_flags = ""
 
-        if (swift_whole_module_optimization) {
-          _extra_flags += " -whole-module-optimization"
-          _objects_dir = "{{target_out_dir}}"
-
-          outputs += [ "$_objects_dir/{{module_name}}.o" ]
-        } else {
-          _objects_dir = "{{target_out_dir}}/{{label_name}}"
-
-          partial_outputs = [ "$_objects_dir/{{source_name_part}}.o" ]
-        }
-
-        _env_vars = "TOOL_VERSION=${tool_versions.swiftc}"
-        if (invoker.sdk_developer_dir != "") {
-          _env_vars += " DEVELOPER_DIR=${toolchain_args.sdk_developer_dir}"
-        }
-
-        # Starting with version 5.6, the Swift compiler will always
-        # generates precompiled headers. In anterior version, it was
-        # used when bridging headers and whole module optimisation
-        # where enabled, and it could be disabled with the parameter
-        # `-disable-bridging-pch`.
-        #
-        # The precompiled headers are binary files (i.e. they are not
-        # regular Objective-C header files and cannot be loaded as such).
-        #
-        # There is an hidden requirements that the compiler needs to
-        # be told where to save those .pch files (via the parameter
-        # `-pch-output-dir $dir`). If this parameter is not passed, the
-        # compiler will silently write them at an incorrect location,
-        # leading later pass to try to load those .pch files as either
-        # regular header files (.h) or object files (.o) and causing
-        # to compilation failures.
-        #
-        # List the directory where the precompiled header is generated
-        # as an output, but do not list the .pch file itself. This is
-        # because the names includes two hashes (one corresponding to
-        # the compiler revision, and the other probably derived from
-        # the module itself) that are difficult to generate.
-        #
-        # Still we want to avoid creating a directory that has the same
-        # name as a file generated by another rule, so explicitly list
-        # the directory in `outputs` so that gn can warn it is conflicts
-        # with another output file.
-
-        _pch_output_dir = "{{target_out_dir}}/{{module_name}}:pch/"
-        outputs += [ "$_pch_output_dir/{{module_name}}.stamp" ]
+        # Environment variables passed to the wrapper script. Considered
+        # part of the command-line by ninja (and thus cause the build to
+        # be considered dirty if they change) without having to be parsed
+        # by the script.
+        _env_vars = "TOOL_VERSION=${tool_versions.swiftc} " +
+                    "JSON_VERSION=${tool_versions.const_extract_protocols}"
 
         # Include the version of the compiler on the command-line. This causes
         # `ninja` to consider all the compilation output to be dirty when the
         # version changes.
         if (defined(swiftc_version)) {
-          _extra_flags += " -swiftc-version $swiftc_version"
+          _env_vars += " SWIFTC_VERSION=$swiftc_version"
         }
 
         # Include the version of Xcode on the command-line (if specified via
@@ -724,7 +721,11 @@
         # changes, they may encode dependency on now non-existing frameworks
         # causing linker failures ultimately.
         if (defined(toolchain_args.xcode_build)) {
-          _extra_flags += " -xcode-version ${toolchain_args.xcode_build}"
+          _env_vars += " XCODE_VERSION=${toolchain_args.xcode_build}"
+        }
+
+        if (invoker.sdk_developer_dir != "") {
+          _env_vars += " DEVELOPER_DIR=${toolchain_args.sdk_developer_dir}"
         }
 
         if (swift_toolchain_path != "") {
@@ -732,6 +733,14 @@
                           rebase_path(swift_toolchain_path, root_build_dir)
         }
 
+        if (swift_whole_module_optimization) {
+          _extra_flags += " -whole-module-optimization"
+        }
+
+        if (swift_keep_intermediate_files) {
+          _extra_flags += " -derived-data-dir $_derived_data_dir"
+        }
+
         # The Swift compiler assumes that the generated header will be used by
         # Objective-C code compiled with module support enabled (-fmodules).
         #
@@ -758,16 +767,20 @@
         # around those two issues.
         _extra_flags += " -fix-module-imports"
 
+        _src_dir = rebase_path("//", root_build_dir)
+        _const_gather_protocols_file =
+            rebase_path("//build/toolchain/ios/const_extract_protocols.json",
+                        root_build_dir)
+
         command =
             "$_env_vars $python_path $_tool -module-name {{module_name}} " +
-            "-root-dir " + rebase_path("//", root_build_dir) + " " +
-            "-object-dir $_objects_dir -pch-output-dir $_pch_output_dir " +
-            "-module-path {{target_gen_dir}}/{{module_name}}.swiftmodule " +
-            "-header-path {{target_gen_dir}}/{{target_output_name}}.h " +
-            "-depfile {{target_out_dir}}/{{module_name}}.d " +
-            "-bridge-header {{bridge_header}} $_extra_flags " +
-            "{{swiftflags}} {{include_dirs}} {{module_dirs}} {{inputs}}"
-        description = "SWIFT {{output}}"
+            "-header-path $_header_path -target-out-dir $_output_dir " +
+            "-const-gather-protocols-file $_const_gather_protocols_file " +
+            "-depfile-path $depfile -src-dir $_src_dir -bridge-header " +
+            "{{bridge_header}} {{include_dirs}} {{module_dirs}} " +
+            "{{swiftflags}} {{inputs}}$_extra_flags"
+
+        description = "SWIFT $_output_dir/{{module_name}}.swiftmodule"
       }
     }
 
diff --git a/build/toolchain/ios/const_extract_protocols.json b/build/toolchain/ios/const_extract_protocols.json
new file mode 100644
index 0000000..fffab8ac
--- /dev/null
+++ b/build/toolchain/ios/const_extract_protocols.json
@@ -0,0 +1 @@
+["AppIntent","EntityQuery","AppEntity","TransientEntity","AppEnum","AppShortcutProviding","AppShortcutsProvider","AnyResolverProviding","AppIntentsPackage","DynamicOptionsProvider"]
\ No newline at end of file
diff --git a/build/toolchain/ios/swiftc.py b/build/toolchain/ios/swiftc.py
index b8f7b5d4..85d2728 100644
--- a/build/toolchain/ios/swiftc.py
+++ b/build/toolchain/ios/swiftc.py
@@ -3,13 +3,275 @@
 # found in the LICENSE file.
 
 import argparse
+import collections
+import contextlib
+import hashlib
+import io
 import json
+import multiprocessing
 import os
+import shutil
 import subprocess
 import sys
 import tempfile
 
 
+class ArgumentForwarder(object):
+  """Class used to abstract forwarding arguments from to the swiftc compiler.
+
+  Arguments:
+    - arg_name: string corresponding to the argument to pass to the compiler
+    - arg_join: function taking the compiler name and returning whether the
+                argument value is attached to the argument or separated
+    - to_swift: function taking the argument value and returning whether it
+                must be passed to the swift compiler
+    - to_clang: function taking the argument value and returning whether it
+                must be passed to the clang compiler
+  """
+
+  def __init__(self, arg_name, arg_join, to_swift, to_clang):
+    self._arg_name = arg_name
+    self._arg_join = arg_join
+    self._to_swift = to_swift
+    self._to_clang = to_clang
+
+  def forward(self, swiftc_args, values, target_triple):
+    if not values:
+      return
+
+    is_catalyst = target_triple.endswith('macabi')
+    for value in values:
+      if self._to_swift(value):
+        if self._arg_join('swift'):
+          swiftc_args.append(f'{self._arg_name}{value}')
+        else:
+          swiftc_args.append(self._arg_name)
+          swiftc_args.append(value)
+
+      if self._to_clang(value) and not is_catalyst:
+        if self._arg_join('clang'):
+          swiftc_args.append('-Xcc')
+          swiftc_args.append(f'{self._arg_name}{value}')
+        else:
+          swiftc_args.append('-Xcc')
+          swiftc_args.append(self._arg_name)
+          swiftc_args.append('-Xcc')
+          swiftc_args.append(value)
+
+
+class IncludeArgumentForwarder(ArgumentForwarder):
+  """Argument forwarder for -I and -isystem."""
+
+  def __init__(self, arg_name):
+    ArgumentForwarder.__init__(self,
+                               arg_name,
+                               arg_join=lambda _: len(arg_name) == 1,
+                               to_swift=lambda _: arg_name != '-isystem',
+                               to_clang=lambda _: True)
+
+
+class FrameworkArgumentForwarder(ArgumentForwarder):
+  """Argument forwarder for -F and -Fsystem."""
+
+  def __init__(self, arg_name):
+    ArgumentForwarder.__init__(self,
+                               arg_name,
+                               arg_join=lambda _: len(arg_name) == 1,
+                               to_swift=lambda _: True,
+                               to_clang=lambda _: True)
+
+
+class DefineArgumentForwarder(ArgumentForwarder):
+  """Argument forwarder for -D."""
+
+  def __init__(self, arg_name):
+    ArgumentForwarder.__init__(self,
+                               arg_name,
+                               arg_join=lambda _: _ == 'clang',
+                               to_swift=lambda _: '=' not in _,
+                               to_clang=lambda _: True)
+
+
+# Dictionary mapping argument names to their ArgumentForwarder.
+ARGUMENT_FORWARDER_FOR_ATTR = (
+    ('include_dirs', IncludeArgumentForwarder('-I')),
+    ('system_include_dirs', IncludeArgumentForwarder('-isystem')),
+    ('framework_dirs', FrameworkArgumentForwarder('-F')),
+    ('system_framework_dirs', FrameworkArgumentForwarder('-Fsystem')),
+    ('defines', DefineArgumentForwarder('-D')),
+)
+
+
+class FileWriter(contextlib.AbstractContextManager):
+  """
+  FileWriter is a file-like object that only write data to disk if changed.
+
+  This object implements the context manager protocols and thus can be used
+  in a with-clause. The data is written to disk when the context is exited,
+  and only if the content is different from current file content.
+
+    with FileWriter(path) as stream:
+      stream.write('...')
+
+  If the with-clause ends with an exception, no data is written to the disk
+  and any existing file is left untouched.
+  """
+
+  def __init__(self, filepath, encoding='utf8'):
+    self._stringio = io.StringIO()
+    self._filepath = filepath
+    self._encoding = encoding
+
+  def __exit__(self, exc_type, exc_value, traceback):
+    if exc_type or exc_value or traceback:
+      return
+
+    new_content = self._stringio.getvalue()
+    if os.path.exists(self._filepath):
+      with open(self._filepath, encoding=self._encoding) as stream:
+        old_content = stream.read()
+
+      if old_content == new_content:
+        return
+
+    with open(self._filepath, 'w', encoding=self._encoding) as stream:
+      stream.write(new_content)
+
+  def write(self, data):
+    self._stringio.write(data)
+
+
+@contextlib.contextmanager
+def existing_directory(path):
+  """Returns a context manager wrapping an existing directory."""
+  yield path
+
+
+def create_stamp_file(path):
+  """Writes an empty stamp file at path."""
+  with FileWriter(path) as stream:
+    stream.write('')
+
+
+def create_build_cache_dir(args, build_signature):
+  """Creates the build cache directory according to `args`.
+
+  This function returns an object that implements the context manager
+  protocol and thus can be used in a with-clause. If -derived-data-dir
+  argument is not used, the returned directory is a temporary directory
+  that will be deleted when the with-clause is exited.
+  """
+  if not args.derived_data_dir:
+    return tempfile.TemporaryDirectory()
+
+  # The derived data cache can be quite large, so delete any obsolete
+  # files or directories.
+  stamp_name = f'{args.module_name}.stamp'
+  if os.path.isdir(args.derived_data_dir):
+    for name in os.listdir(args.derived_data_dir):
+      if name not in (build_signature, stamp_name):
+        path = os.path.join(args.derived_data_dir, name)
+        if os.path.isdir(path):
+          shutil.rmtree(path)
+        else:
+          os.unlink(path)
+
+  ensure_directory(args.derived_data_dir)
+  create_stamp_file(os.path.join(args.derived_data_dir, stamp_name))
+
+  return existing_directory(
+      ensure_directory(os.path.join(args.derived_data_dir, build_signature)))
+
+
+def ensure_directory(path):
+  """Creates directory at `path` if it does not exists."""
+  if not os.path.isdir(path):
+    os.makedirs(path)
+  return path
+
+
+def build_signature(env, args):
+  """Generates the build signature from `env` and `args`.
+
+  This allow re-using the derived data dir between builds while still
+  forcing the data to be recreated from scratch in case of significant
+  changes to the build settings (different arguments or tool versions).
+  """
+  m = hashlib.sha1()
+  for key in sorted(env):
+    if key.endswith('_VERSION') or key == 'DEVELOPER_DIR':
+      m.update(f'{key}={env[key]}'.encode('utf8'))
+  for i, arg in enumerate(args):
+    m.update(f'{i}={arg}'.encode('utf8'))
+  return m.hexdigest()
+
+
+def generate_source_output_file_map_fragment(args, filename):
+  """Generates source OutputFileMap.json fragment according to `args`.
+
+  Create the fragment for a single .swift source file for OutputFileMap.
+  The output depends on whether -whole-module-optimization argument is
+  used or not.
+  """
+  assert os.path.splitext(filename)[1] == '.swift', filename
+  basename = os.path.splitext(os.path.basename(filename))[0]
+  rel_name = os.path.join(args.target_out_dir, basename)
+  out_name = rel_name
+
+  fragment = {
+      'index-unit-output-path': f'/{rel_name}.o',
+      'object': f'{out_name}.o',
+  }
+
+  if not args.whole_module_optimization:
+    fragment.update({
+        'const-values': f'{out_name}.swiftconstvalues',
+        'dependencies': f'{out_name}.d',
+        'diagnostics': f'{out_name}.dia',
+        'swift-dependencies': f'{out_name}.swiftdeps',
+    })
+
+  return fragment
+
+
+def generate_module_output_file_map_fragment(args):
+  """Generates module OutputFileMap.json fragment according to `args`.
+
+  Create the fragment for the module itself for OutputFileMap. The output
+  depends on whether -whole-module-optimization argument is used or not.
+  """
+  out_name = os.path.join(args.target_out_dir, args.module_name)
+
+  if args.whole_module_optimization:
+    fragment = {
+        'const-values': f'{out_name}.swiftconstvalues',
+        'dependencies': f'{out_name}.d',
+        'diagnostics': f'{out_name}.dia',
+        'swift-dependencies': f'{out_name}.swiftdeps',
+    }
+  else:
+    fragment = {
+        'emit-module-dependencies': f'{out_name}.d',
+        'emit-module-diagnostics': f'{out_name}.dia',
+        'swift-dependencies': f'{out_name}.swiftdeps',
+    }
+
+  return fragment
+
+
+def generate_output_file_map(args):
+  """Generates OutputFileMap.json according to `args`.
+
+  Returns the mapping as a python dictionary that can be serialized to
+  disk as JSON.
+  """
+  output_file_map = {'': generate_module_output_file_map_fragment(args)}
+  for filename in args.sources:
+    fragment = generate_source_output_file_map_fragment(args, filename)
+    output_file_map[filename] = fragment
+  return output_file_map
+
+
 def fix_module_imports(header_path, output_path):
   """Convert modules import to work without -fmodules support.
 
@@ -25,308 +287,324 @@
   """
 
   header_contents = []
-  with open(header_path, 'r') as header_file:
+  with open(header_path, 'r', encoding='utf8') as header_file:
+
+    imports_section = None
     for line in header_file:
-      if line == '#if __has_feature(objc_modules)\n':
-        header_contents.append('#if 1  // #if __has_feature(objc_modules)\n')
-        nesting_level = 1
-        for line in header_file:
-          if line == '#endif\n':
-            nesting_level -= 1
-          elif line.startswith('@import'):
-            name = line.split()[1].split(';')[0]
-            if name != 'ObjectiveC':
-              header_contents.append(f'#import <{name}/{name}.h>  ')
-            header_contents.append('// ')
-          elif line.startswith('#if'):
-            nesting_level += 1
+      if line.startswith('#if __has_feature(objc_modules)'):
+        assert imports_section is None
+        imports_section = (len(header_contents) + 1, 1)
+      elif imports_section:
+        section_start, nesting_level = imports_section
+        if line.startswith('#if'):
+          imports_section = (section_start, nesting_level + 1)
+        elif line.startswith('#endif'):
+          if nesting_level > 1:
+            imports_section = (section_start, nesting_level - 1)
+          else:
+            imports_section = None
+            section_end = len(header_contents)
+            header_contents.append('#else\n')
+            for index in range(section_start, section_end):
+              l = header_contents[index]
+              if l.startswith('@import'):
+                name = l.split()[1].split(';')[0]
+                if name != 'ObjectiveC':
+                  header_contents.append(f'#import <{name}/{name}.h>\n')
+              else:
+                header_contents.append(l)
+      header_contents.append(line)
 
-          header_contents.append(line)
-          if nesting_level == 0:
-            break
-      else:
-        header_contents.append(line)
-
-  with open(output_path, 'w') as header_file:
+  with FileWriter(output_path) as header_file:
     for line in header_contents:
       header_file.write(line)
 
 
-def compile_module(module, sources, settings, extras, tmpdir):
-  """Compile `module` from `sources` using `settings`."""
-  output_file_map = {}
-  if settings.whole_module_optimization:
-    output_file_map[''] = {
-        'object': os.path.join(settings.object_dir, module + '.o'),
-        'dependencies': os.path.join(tmpdir, module + '.d'),
-    }
-  else:
-    for source in sources:
-      name, _ = os.path.splitext(os.path.basename(source))
-      output_file_map[source] = {
-          'object': os.path.join(settings.object_dir, name + '.o'),
-          'dependencies': os.path.join(tmpdir, name + '.d'),
-      }
+def invoke_swift_compiler(args, extras_args, build_cache_dir, output_file_map):
+  """Invokes Swift compiler to compile module according to `args`.
 
-  for key in ('module_path', 'header_path', 'depfile'):
-    path = getattr(settings, key)
-    if os.path.exists(path):
-      os.unlink(path)
-    if key == 'module_path':
-      for ext in '.swiftdoc', '.swiftsourceinfo':
-        path = os.path.splitext(getattr(settings, key))[0] + ext
-        if os.path.exists(path):
-          os.unlink(path)
-    directory = os.path.dirname(path)
-    if not os.path.exists(directory):
-      os.makedirs(directory)
+  The `build_cache_dir` and `output_file_map` should be path to existing
+  directory to use for writing intermediate build artifact (optionally
+  a temporary directory) and path to $module-OutputFileMap.json file that
+  lists the outputs to generate for the module and each source file.
 
-  if not os.path.exists(settings.object_dir):
-    os.makedirs(settings.object_dir)
+  If -fix-module-imports argument is passed, the generated header for the
+  module is written to a temporary location and then modified to replace
+  @import by corresponding #import.
+  """
 
-  if not os.path.exists(settings.pch_output_dir):
-    os.makedirs(settings.pch_output_dir)
+  # Write the $module.SwiftFileList file.
+  swift_file_list_path = os.path.join(args.target_out_dir,
+                                      f'{args.module_name}.SwiftFileList')
 
-  stamp = os.path.join(settings.pch_output_dir, f'{module}.stamp')
-  if os.path.exists(stamp):
-    os.unlink(stamp)
-  with open(stamp, 'w') as stamp_file:
-    stamp_file.write('')
+  with FileWriter(swift_file_list_path) as stream:
+    for filename in sorted(args.sources):
+      stream.write(filename)
+      stream.write('\n')
 
-  for key in output_file_map:
-    path = output_file_map[key]['object']
-    if os.path.exists(path):
-      os.unlink(path)
+  header_path = args.header_path
+  if args.fix_module_imports:
+    header_path = os.path.join(build_cache_dir, os.path.basename(header_path))
 
-  output_file_map.setdefault('', {})['swift-dependencies'] = \
-      os.path.join(tmpdir, module + '.swift.d')
-
-  output_file_map_path = os.path.join(tmpdir, module + '.json')
-  with open(output_file_map_path, 'w') as output_file_map_file:
-    output_file_map_file.write(json.dumps(output_file_map))
-    output_file_map_file.flush()
-
-  extra_args = []
-  if settings.file_compilation_dir:
-    extra_args.extend([
-        '-file-compilation-dir',
-        settings.file_compilation_dir,
-    ])
-
-  if settings.bridge_header:
-    extra_args.extend([
-        '-import-objc-header',
-        os.path.abspath(settings.bridge_header),
-    ])
-
-  if settings.whole_module_optimization:
-    extra_args.append('-whole-module-optimization')
-
-  if settings.target:
-    extra_args.extend([
-        '-target',
-        settings.target,
-    ])
-
-  if settings.sdk:
-    extra_args.extend([
-        '-sdk',
-        os.path.abspath(settings.sdk),
-    ])
-
-  if settings.swift_version:
-    extra_args.extend([
-        '-swift-version',
-        settings.swift_version,
-    ])
-
-  if settings.include_dirs:
-    for include_dir in settings.include_dirs:
-      extra_args.append('-I' + include_dir)
-
-  if settings.system_include_dirs:
-    for system_include_dir in settings.system_include_dirs:
-      extra_args.extend(['-Xcc', '-isystem', '-Xcc', system_include_dir])
-
-  if settings.framework_dirs:
-    for framework_dir in settings.framework_dirs:
-      extra_args.extend([
-          '-F',
-          framework_dir,
-      ])
-
-  if settings.system_framework_dirs:
-    for system_framework_dir in settings.system_framework_dirs:
-      extra_args.extend([
-          '-F',
-          system_framework_dir,
-      ])
-
-  if settings.enable_cxx_interop:
-    extra_args.extend([
-        '-Xfrontend',
-        '-enable-cxx-interop',
-    ])
-
-  # The swiftc compiler uses a global module cache that is not robust against
-  # changes in the sub-modules nor against corruption (see crbug.com/1358073).
-  # Force the compiler to store the module cache in a sub-directory of `tmpdir`
-  # to ensure a pristine module cache is used for every compiler invocation.
-  module_cache_path = os.path.join(tmpdir, settings.swiftc_version,
-                                   'ModuleCache')
-
-  # If the generated header is post-processed, generate it to a temporary
-  # location (to avoid having the file appear to suddenly change).
-  if settings.fix_module_imports:
-    header_path = os.path.join(tmpdir, f'{module}.h')
-  else:
-    header_path = settings.header_path
-
-  process = subprocess.Popen([
-      settings.swift_toolchain_path + '/usr/bin/swiftc',
+  swiftc_args = [
       '-parse-as-library',
       '-module-name',
-      module,
-      '-module-cache-path',
-      module_cache_path,
-      '-emit-object',
+      args.module_name,
+      f'@{swift_file_list_path}',
+      '-sdk',
+      args.sdk_path,
+      '-target',
+      args.target_triple,
+      '-swift-version',
+      args.swift_version,
+      '-c',
+      '-output-file-map',
+      output_file_map,
+      '-save-temps',
+      '-no-color-diagnostics',
+      '-serialize-diagnostics',
       '-emit-dependencies',
       '-emit-module',
       '-emit-module-path',
-      settings.module_path,
+      os.path.join(args.target_out_dir, f'{args.module_name}.swiftmodule'),
       '-emit-objc-header',
       '-emit-objc-header-path',
       header_path,
-      '-output-file-map',
-      output_file_map_path,
+      '-working-directory',
+      os.getcwd(),
+      '-index-store-path',
+      ensure_directory(os.path.join(build_cache_dir, 'Index.noindex')),
+      '-module-cache-path',
+      ensure_directory(os.path.join(build_cache_dir, 'ModuleCache.noindex')),
       '-pch-output-dir',
-      os.path.abspath(settings.pch_output_dir),
-  ] + extra_args + extras + sources)
+      ensure_directory(os.path.join(build_cache_dir, 'PrecompiledHeaders')),
+  ]
 
+  # Handle optional -bridge-header flag.
+  if args.bridge_header:
+    swiftc_args.extend(('-import-objc-header', args.bridge_header))
+
+  # Handle swift const values extraction.
+  swiftc_args.extend(['-emit-const-values'])
+  swiftc_args.extend([
+      '-Xfrontend',
+      '-const-gather-protocols-file',
+      '-Xfrontend',
+      args.const_gather_protocols_file,
+  ])
+
+  # Handle -I, -F, -isystem, -Fsystem and -D arguments.
+  for (attr_name, forwarder) in ARGUMENT_FORWARDER_FOR_ATTR:
+    forwarder.forward(swiftc_args, getattr(args, attr_name), args.target_triple)
+
+  # Handle -whole-module-optimization flag.
+  num_threads = max(1, multiprocessing.cpu_count() // 2)
+  if args.whole_module_optimization:
+    swiftc_args.extend([
+        '-whole-module-optimization',
+        '-no-emit-module-separately-wmo',
+        '-num-threads',
+        f'{num_threads}',
+    ])
+  else:
+    swiftc_args.extend([
+        '-enable-batch-mode',
+        '-incremental',
+        '-experimental-emit-module-separately',
+        '-disable-cmo',
+        f'-j{num_threads}',
+    ])
+
+  swift_toolchain_path = args.swift_toolchain_path
+  if not swift_toolchain_path:
+    swift_toolchain_path = os.path.join(os.path.dirname(args.sdk_path),
+                                        'XcodeDefault.xctoolchain')
+    if not os.path.isdir(swift_toolchain_path):
+      swift_toolchain_path = ''
+
+  command = [f'{swift_toolchain_path}/usr/bin/swiftc'] + swiftc_args
+  if extras_args:
+    command.extend(extras_args)
+
+  process = subprocess.Popen(command)
   process.communicate()
+
   if process.returncode:
     sys.exit(process.returncode)
 
-  if settings.fix_module_imports:
-    fix_module_imports(header_path, settings.header_path)
+  if args.fix_module_imports:
+    fix_module_imports(header_path, args.header_path)
 
-  # The swiftc compiler generates depfile that uses absolute paths, but
-  # ninja requires paths in depfiles to be identical to paths used in
-  # the build.ninja files.
-  #
-  # Since gn generates paths relative to the build directory for all paths
-  # below the repository checkout, we need to convert those to relative
-  # paths.
-  #
-  # See https://crbug.com/1287114 for build failure that happen when the
-  # paths in the depfile are kept absolute.
-  out_dir = os.getcwd() + os.path.sep
-  src_dir = os.path.abspath(settings.root_dir) + os.path.sep
 
-  depfile_content = dict()
-  for key in output_file_map:
+def generate_depfile(args, output_file_map):
+  """Generates compilation depfile according to `args`.
 
-    # When whole module optimisation is disabled, there will be an entry
-    # with an empty string as the key and only ('swift-dependencies') as
-    # keys in the value dictionary. This is expected, so skip entry that
-    # do not include 'dependencies' in their keys.
-    depencency_file_path = output_file_map[key].get('dependencies')
-    if not depencency_file_path:
-      continue
+  Parses all intermediate depfile generated by the Swift compiler and
+  replaces absolute path by relative paths (since ninja compares paths
+  as strings and does not resolve relative paths to absolute).
 
-    for line in open(depencency_file_path):
-      output, inputs = line.split(' : ', 2)
-      _, ext = os.path.splitext(output)
-      if ext == '.o':
-        key = output
-      else:
-        key = os.path.splitext(settings.module_path)[0] + ext
-      if key not in depfile_content:
-        depfile_content[key] = set()
-      for path in inputs.split():
-        if path.startswith(src_dir) or path.startswith(out_dir):
-          path = os.path.relpath(path, out_dir)
-        depfile_content[key].add(path)
+  Converts path to the SDK and toolchain files to the sdk/xcode_link
+  symlinks if possible and available.
+  """
+  xcode_paths = {}
+  if os.path.islink(args.sdk_path):
+    xcode_links = os.path.dirname(args.sdk_path)
+    for link_name in os.listdir(xcode_links):
+      link_path = os.path.join(xcode_links, link_name)
+      if os.path.islink(link_path):
+        xcode_paths[os.path.realpath(link_path) + os.sep] = link_path + os.sep
 
-  with open(settings.depfile, 'w') as depfile:
-    keys = sorted(depfile_content.keys())
-    for key in sorted(keys):
-      depfile.write('%s : %s\n' % (key, ' '.join(sorted(depfile_content[key]))))
+  out_dir = os.getcwd() + os.sep
+  src_dir = os.path.abspath(args.src_dir) + os.path.sep
+
+  depfile_content = collections.defaultdict(set)
+  for value in output_file_map.values():
+    partial_depfile_path = value.get('dependencies', None)
+    if partial_depfile_path:
+      with open(partial_depfile_path, encoding='utf8') as stream:
+        for line in stream:
+          output, inputs = line.split(' : ', 2)
+          output = os.path.relpath(output, out_dir)
+
+          for path in inputs.split():
+            for xcode_path in xcode_paths:
+              if path.startswith(xcode_path):
+                path = xcode_paths[xcode_path] + path[len(xcode_path):]
+            if path.startswith(src_dir) or path.startswith(out_dir):
+              path = os.path.relpath(path, out_dir)
+            depfile_content[output].add(path)
+
+  with FileWriter(args.depfile_path) as stream:
+    for output, inputs in sorted(depfile_content.items()):
+      stream.write(f'{output}: {" ".join(sorted(inputs))}\n')
+
+
+def compile_module(args, extras_args, build_signature):
+  """Compiles Swift module according to `args`."""
+  for path in (args.target_out_dir, os.path.dirname(args.header_path)):
+    ensure_directory(path)
+
+  # Write the $module-OutputFileMap.json file.
+  output_file_map = generate_output_file_map(args)
+  output_file_map_path = os.path.join(args.target_out_dir,
+                                      f'{args.module_name}-OutputFileMap.json')
+
+  with FileWriter(output_file_map_path) as stream:
+    json.dump(output_file_map, stream, indent=' ', sort_keys=True)
+
+  # Invoke Swift compiler.
+  with create_build_cache_dir(args, build_signature) as build_cache_dir:
+    invoke_swift_compiler(args,
+                          extras_args,
+                          build_cache_dir=build_cache_dir,
+                          output_file_map=output_file_map_path)
+
+  # Generate the depfile.
+  generate_depfile(args, output_file_map)
 
 
 def main(args):
-  parser = argparse.ArgumentParser(add_help=False)
-  parser.add_argument('-module-name', help='name of the Swift module')
-  parser.add_argument('-include',
-                      '-I',
+  parser = argparse.ArgumentParser(allow_abbrev=False, add_help=False)
+
+  # Required arguments.
+  parser.add_argument('-module-name',
+                      required=True,
+                      help='name of the Swift module')
+
+  parser.add_argument('-src-dir',
+                      required=True,
+                      help='path to the source directory')
+
+  parser.add_argument('-target-out-dir',
+                      required=True,
+                      help='path to the object directory')
+
+  parser.add_argument('-header-path',
+                      required=True,
+                      help='path to the generated header file')
+
+  parser.add_argument('-bridge-header',
+                      required=True,
+                      help='path to the Objective-C bridge header file')
+
+  parser.add_argument('-depfile-path',
+                      required=True,
+                      help='path to the output dependency file')
+
+  # Optional arguments.
+  parser.add_argument('-derived-data-dir',
+                      help='path to the derived data directory')
+
+  # Required arguments (forwarded to the Swift compiler).
+  parser.add_argument('-target',
+                      required=True,
+                      dest='target_triple',
+                      help='generate code for the given target')
+
+  parser.add_argument('-sdk',
+                      required=True,
+                      dest='sdk_path',
+                      help='path to the iOS SDK')
+
+  parser.add_argument('-const-gather-protocols-file',
+                      required=True,
+                      help='path to file containing const values protocols')
+
+  # Optional arguments.
+  parser.add_argument('-fix-module-imports',
+                      default=False,
+                      action='store_true',
+                      help='fix module imports in generated header')
+
+  parser.add_argument('-swift-toolchain-path',
+                      default='',
+                      help='path to the Swift toolchain to use')
+
+  # Optional arguments (forwarded to the Swift compiler).
+  parser.add_argument('-I',
                       action='append',
                       dest='include_dirs',
                       help='add directory to header search path')
+
   parser.add_argument('-isystem',
                       action='append',
                       dest='system_include_dirs',
                       help='add directory to system header search path')
-  parser.add_argument('sources', nargs='+', help='Swift source file to compile')
-  parser.add_argument('-whole-module-optimization',
-                      action='store_true',
-                      help='enable whole module optimization')
-  parser.add_argument('-object-dir',
-                      help='path to the generated object files directory')
-  parser.add_argument('-pch-output-dir',
-                      help='path to directory where .pch files are saved')
-  parser.add_argument('-module-path', help='path to the generated module file')
-  parser.add_argument('-header-path', help='path to the generated header file')
-  parser.add_argument('-bridge-header',
-                      help='path to the Objective-C bridge header')
-  parser.add_argument('-depfile', help='path to the generated depfile')
-  parser.add_argument('-swift-version',
-                      help='version of Swift language to support')
-  parser.add_argument('-target',
-                      action='store',
-                      help='generate code for the given target <triple>')
-  parser.add_argument('-sdk', action='store', help='compile against sdk')
+
   parser.add_argument('-F',
+                      action='append',
                       dest='framework_dirs',
-                      action='append',
-                      help='add dir to framework search path')
+                      help='add directory to framework search path')
+
   parser.add_argument('-Fsystem',
-                      '-iframework',
-                      dest='system_framework_dirs',
                       action='append',
-                      help='add dir to system framework search path')
-  parser.add_argument('-root-dir',
-                      dest='root_dir',
-                      action='store',
-                      required=True,
-                      help='path to the root of the repository')
-  parser.add_argument('-swift-toolchain-path',
-                      default='',
-                      action='store',
-                      dest='swift_toolchain_path',
-                      help='path to the root of the Swift toolchain')
+                      dest='system_framework_dirs',
+                      help='add directory to system framework search path')
+
+  parser.add_argument('-D',
+                      action='append',
+                      dest='defines',
+                      help='add preprocessor define')
+
+  parser.add_argument('-swift-version',
+                      default='5',
+                      help='version of the Swift language')
+
+  parser.add_argument('-whole-module-optimization',
+                      default=False,
+                      action='store_true',
+                      help='enable whole module optimisation')
+
   parser.add_argument('-file-compilation-dir',
-                      default='',
-                      action='store',
-                      help='compilation directory to embed in the debug info')
-  parser.add_argument('-enable-cxx-interop',
-                      dest='enable_cxx_interop',
-                      action='store_true',
-                      help='allow importing C++ modules into Swift')
-  parser.add_argument('-fix-module-imports',
-                      action='store_true',
-                      help='enable hack to fix module imports')
-  parser.add_argument('-swiftc-version',
-                      default='',
-                      action='store',
-                      help='version of swiftc compiler')
-  parser.add_argument('-xcode-version',
-                      default='',
-                      action='store',
-                      help='version of xcode')
+                      help='compilation directory to embed in debug info')
+
+  # Positional arguments.
+  parser.add_argument('sources',
+                      nargs='+',
+                      help='Swift source files to compile')
 
   parsed, extras = parser.parse_known_args(args)
-  with tempfile.TemporaryDirectory() as tmpdir:
-    compile_module(parsed.module_name, parsed.sources, parsed, extras, tmpdir)
+  compile_module(parsed, extras, build_signature(os.environ, args))
 
 
 if __name__ == '__main__':