[ios] Add example of building swift on iOS

The CL also adds necessary scaffolding to build some .swift files
in examples/ios (to teach how the tool needs to be used).

Bug: 121
Change-Id: I46b553c022484130b867547b2cdedc853689a46b
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/9541
Commit-Queue: Sylvain Defresne <sdefresne@chromium.org>
Reviewed-by: Brett Wilson <brettw@chromium.org>
diff --git a/examples/ios/app/AppDelegate.m b/examples/ios/app/AppDelegate.m
index 32b3d44..b6f94fb 100644
--- a/examples/ios/app/AppDelegate.m
+++ b/examples/ios/app/AppDelegate.m
@@ -4,10 +4,13 @@
 
 #import "app/AppDelegate.h"
 
+#import "app/Foo.h"
+
 @implementation AppDelegate
 
 - (BOOL)application:(UIApplication*)application
     didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+  NSLog(@"%@", [[[FooWrapper alloc] init] helloWithName:@"World"]);
   return YES;
 }
 
diff --git a/examples/ios/app/BUILD.gn b/examples/ios/app/BUILD.gn
index eb4b8a9..fdb3d03 100644
--- a/examples/ios/app/BUILD.gn
+++ b/examples/ios/app/BUILD.gn
@@ -27,6 +27,7 @@
   ]
 
   deps = [
+    ":foo",
     ":storyboards",
     "//shared:hello_framework",
     "//shared:hello_framework+bundle",
@@ -39,3 +40,28 @@
     "resources/Main.storyboard",
   ]
 }
+
+source_set("baz") {
+  module_name = "Baz"
+  sources = [ "Baz.swift" ]
+}
+
+source_set("bar") {
+  module_name = "Bar"
+  sources = [ "Bar.swift" ]
+  deps = [ ":baz" ]
+}
+
+group("bar_indirect") {
+  public_deps = [ ":bar" ]
+}
+
+source_set("foo") {
+  module_name = "Foo"
+  bridge_header = "Foo-Bridging-Header.h"
+  sources = [
+    "Foo.swift",
+    "FooWrapper.swift",
+  ]
+  deps = [ ":bar_indirect" ]
+}
diff --git a/examples/ios/app/Bar.swift b/examples/ios/app/Bar.swift
new file mode 100644
index 0000000..9e35c9d
--- /dev/null
+++ b/examples/ios/app/Bar.swift
@@ -0,0 +1,8 @@
+
+import Baz;
+
+public class Greeter {
+  public static func greet(greeting: String, name: String, from: String) -> String {
+    return greeting + ", " + name + " (from " + from + ")";
+  }
+}
diff --git a/examples/ios/app/Baz.swift b/examples/ios/app/Baz.swift
new file mode 100644
index 0000000..f389578
--- /dev/null
+++ b/examples/ios/app/Baz.swift
@@ -0,0 +1,2 @@
+
+class Baz {}
diff --git a/examples/ios/app/Foo-Bridging-Header.h b/examples/ios/app/Foo-Bridging-Header.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/examples/ios/app/Foo-Bridging-Header.h
diff --git a/examples/ios/app/Foo.swift b/examples/ios/app/Foo.swift
new file mode 100644
index 0000000..5e9faa1
--- /dev/null
+++ b/examples/ios/app/Foo.swift
@@ -0,0 +1,12 @@
+
+import Bar
+
+class Foo {
+  var name: String;
+  public init(name: String) {
+    self.name = name;
+  }
+  public func hello(name: String) -> String {
+    return Greeter.greet(greeting: "Hello", name: name, from: self.name);
+  }
+}
diff --git a/examples/ios/app/FooWrapper.swift b/examples/ios/app/FooWrapper.swift
new file mode 100644
index 0000000..a82c8a8
--- /dev/null
+++ b/examples/ios/app/FooWrapper.swift
@@ -0,0 +1,10 @@
+
+import Foundation;
+
+@objc
+public class FooWrapper : NSObject {
+  @objc
+  public func hello(name: String) -> String {
+    return Foo(name: "Foo").hello(name: name);
+  }
+}
diff --git a/examples/ios/build/BUILD.gn b/examples/ios/build/BUILD.gn
index aa51efa..2831117 100644
--- a/examples/ios/build/BUILD.gn
+++ b/examples/ios/build/BUILD.gn
@@ -2,6 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/config/ios/deployment_target.gni")
+
 config("compiler") {
   configs = [
     ":include_dirs",
@@ -9,11 +11,16 @@
     ":objc_use_arc",
     ":objc_abi_version",
   ]
+  cflags = [ "-g" ]
+  swiftflags = [ "-g" ]
 }
 
 config("shared_binary") {
   if (current_os == "ios" || current_os == "mac") {
-    configs = [ ":rpath_config" ]
+    configs = [
+      ":rpath_config",
+      ":swift_libdir",
+    ]
   }
 }
 
@@ -31,7 +38,7 @@
 config("include_dirs") {
   include_dirs = [
     "//",
-    root_out_dir,
+    root_gen_dir,
   ]
 }
 
@@ -65,4 +72,17 @@
       "@loader_path/Frameworks",
     ]
   }
+
+  _sdk_info = exec_script("//build/config/ios/scripts/sdk_info.py",
+                          [
+                            "--target-cpu",
+                            current_cpu,
+                            "--deployment-target",
+                            ios_deployment_target,
+                          ],
+                          "json")
+
+  config("swift_libdir") {
+    lib_dirs = [ "${_sdk_info.sdk_path}/usr/lib/swift" ]
+  }
 }
diff --git a/examples/ios/build/toolchain/apple/swiftc.py b/examples/ios/build/toolchain/apple/swiftc.py
new file mode 100755
index 0000000..88ae6e5
--- /dev/null
+++ b/examples/ios/build/toolchain/apple/swiftc.py
@@ -0,0 +1,183 @@
+#!/usr/bin/python3
+
+
+import argparse
+import collections
+import json
+import os
+import subprocess
+import sys
+import tempfile
+
+
+class OrderedSet(collections.OrderedDict):
+
+  def add(self, value):
+    self[value] = True
+
+
+def compile_module(module, sources, settings, extras, tmpdir):
+  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'),
+      }
+
+  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)
+
+  if not os.path.exists(settings.object_dir):
+    os.makedirs(settings.object_dir)
+
+  for key in output_file_map:
+    path = output_file_map[key]['object']
+    if os.path.exists(path):
+      os.unlink(path)
+
+  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.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.include_dirs:
+    for include_dir in settings.include_dirs:
+      extra_args.append('-I' + include_dir)
+
+  process = subprocess.Popen(
+      ['swiftc',
+       '-parse-as-library',
+       '-module-name',
+       module,
+       '-emit-object',
+       '-emit-dependencies',
+       '-emit-module',
+       '-emit-module-path',
+       settings.module_path,
+       '-emit-objc-header',
+       '-emit-objc-header-path',
+       settings.header_path,
+       '-output-file-map',
+       output_file_map_path,
+      ] + extra_args + extras + sources,
+      stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+      universal_newlines=True)
+
+  stdout, stderr = process.communicate()
+  if process.returncode:
+    sys.stdout.write(stdout)
+    sys.stderr.write(stderr)
+    sys.exit(process.returncode)
+
+
+  depfile_content = collections.OrderedDict()
+  for key in output_file_map:
+    for line in open(output_file_map[key]['dependencies']):
+      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] = OrderedSet()
+      for path in inputs.split():
+        depfile_content[key].add(path)
+
+  with open(settings.depfile, 'w') as depfile:
+    for key in depfile_content:
+      if not settings.depfile_filter or key in settings.depfile_filter:
+        inputs = depfile_content[key]
+        depfile.write('%s : %s\n' % (key, ' '.join(inputs)))
+
+
+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', action='append', dest='include_dirs',
+      help='add directory to 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', '-o',
+      help='path to the generated object files directory')
+  parser.add_argument(
+      '--module-path', '-m',
+      help='path to the generated module file')
+  parser.add_argument(
+      '--header-path', '-h',
+      help='path to the generated header file')
+  parser.add_argument(
+      '--bridge-header', '-b',
+      help='path to the Objective-C bridge header')
+  parser.add_argument(
+      '--depfile', '-d',
+      help='path to the generated depfile')
+  parser.add_argument(
+      '--depfile-filter', action='append',
+      help='limit depfile to those files')
+  parser.add_argument(
+      '--target', action='store',
+      help='generate code for the given target <triple>')
+  parser.add_argument(
+      '--sdk', action='store',
+      help='compile against sdk')
+
+  parsed, extras = parser.parse_known_args(args)
+  with tempfile.TemporaryDirectory() as tmpdir:
+    compile_module(
+        parsed.module_name,
+        parsed.sources,
+        parsed,
+        extras,
+        tmpdir)
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))
diff --git a/examples/ios/build/toolchain/ios/BUILD.gn b/examples/ios/build/toolchain/ios/BUILD.gn
index b4e0869..12d246b 100644
--- a/examples/ios/build/toolchain/ios/BUILD.gn
+++ b/examples/ios/build/toolchain/ios/BUILD.gn
@@ -4,6 +4,12 @@
 
 import("//build/config/ios/deployment_target.gni")
 
+declare_args() {
+  # Configure whether whole module optimization is enabled when compiling
+  # swift modules.
+  swift_whole_module_optimization = true
+}
+
 template("ios_toolchain") {
   toolchain(target_name) {
     assert(defined(invoker.toolchain_args),
@@ -25,13 +31,15 @@
     cc = "clang -target ${_sdk_info.target} -isysroot ${_sdk_info.sdk_path}"
     cxx = "clang++ -target ${_sdk_info.target} -isysroot ${_sdk_info.sdk_path}"
 
+    swiftmodule_switch = "-Wl,-add_ast_path,"
+
     tool("link") {
       output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
       rspfile = output + ".rsp"
       rspfile_content = "{{inputs_newline}}"
 
       outputs = [ output ]
-      command = "$cxx {{ldflags}} -o $output -Wl,-filelist,$rspfile {{libs}} {{solibs}} {{frameworks}}"
+      command = "$cxx {{ldflags}} -o $output -Wl,-filelist,$rspfile {{libs}} {{solibs}} {{frameworks}} {{swiftmodules}}"
       description = "LINK {{output}}"
 
       default_output_dir = "{{root_out_dir}}"
@@ -45,7 +53,7 @@
       rspfile_content = "{{inputs_newline}}"
 
       outputs = [ dylib ]
-      command = "$cxx -dynamiclib {{ldflags}} -o $dylib -Wl,-filelist,$rspfile {{libs}} {{solibs}} {{frameworks}}"
+      command = "$cxx -dynamiclib {{ldflags}} -o $dylib -Wl,-filelist,$rspfile {{libs}} {{solibs}} {{frameworks}} {{swiftmodules}}"
       description = "SOLINK {{output}}"
 
       default_output_dir = "{{root_out_dir}}"
@@ -95,9 +103,38 @@
     }
 
     tool("copy_bundle_data") {
-      command = "rm -rf {{output}} && cp -a {{source}} {{output}}"
+      command = "rm -rf {{output}} && cp -PR {{source}} {{output}}"
       description = "COPY_BUNDLE_DATA {{output}}"
     }
+
+    tool("swift") {
+      depfile = "{{target_out_dir}}/{{module_name}}.d"
+      depsformat = "gcc"
+
+      outputs = [
+        # The module needs to be the first output to ensure the
+        # depfile generate works correctly with ninja < 1.9.0.
+        "{{target_gen_dir}}/{{module_name}}.swiftmodule",
+
+        "{{target_gen_dir}}/{{module_name}}.h",
+        "{{target_gen_dir}}/{{module_name}}.swiftdoc",
+        "{{target_gen_dir}}/{{module_name}}.swiftsourceinfo",
+      ]
+
+      if (swift_whole_module_optimization) {
+        _extra_flag = "--whole-module-optimization"
+        _object_dir = "{{target_out_dir}}"
+        outputs += [ "{{target_out_dir}}/{{module_name}}.o" ]
+      } else {
+        _extra_flag = ""
+        _object_dir = "{{target_out_dir}}/{{label_name}}"
+        partial_outputs =
+            [ "{{target_out_dir}}/{{label_name}}/{{source_name_part}}.o" ]
+      }
+
+      _swiftc = rebase_path("//build/toolchain/apple/swiftc.py", root_build_dir)
+      command = "$_swiftc --target ${_sdk_info.target} --sdk ${_sdk_info.sdk_path} --module-name {{module_name}} --object-dir $_object_dir --module-path {{target_gen_dir}}/{{module_name}}.swiftmodule --header-path {{target_gen_dir}}/{{module_name}}.h --depfile {{target_out_dir}}/{{module_name}}.d --depfile-filter {{target_gen_dir}}/{{module_name}}.swiftmodule --bridge-header {{bridge_header}} $_extra_flag {{defines}} {{swiftflags}} {{include_dirs}} {{module_dirs}} {{inputs}}"
+    }
   }
 }