From 073484bde0ac31e96781e5e98b52729aa51e56ad Mon Sep 17 00:00:00 2001 From: Mike Schore Date: Mon, 8 Jul 2019 13:24:03 -0700 Subject: [PATCH] library: migrate public/feature layer of iOS library to Swift Co-authored-by: Keith Smiley keithbsmiley@gmail.com Signed-off-by: Mike Schore mike.schore@gmail.com Description: We've decided to implement our public and platform-specific library layer primarily in Swift on iOS. Migrating to this has some repercussions for the build and for the time-being will require custom Bazel rules to compose the distributable static framework. This PR migrates the code to swift and introduces the build rules required. Note: this PR breaks objective-c. This is known and an issue has been filed #230. Risk Level: Medium - moves pieces of the library to swift, and introduces new build rules. Testing: CI Signed-off-by: Mike Schore Signed-off-by: JP Simard --- mobile/BUILD | 10 +- mobile/azure-pipelines.yml | 70 +++---- mobile/bazel/swift_static_framework.bzl | 183 ++++++++++++++++++ mobile/dist/BUILD | 2 + .../docs/root/start/examples/hello_world.rst | 5 + .../objective-c/hello_world/AppDelegate.mm | 2 +- mobile/library/objective-c/BUILD | 16 +- mobile/library/objective-c/EnvoyEngine.h | 14 ++ mobile/library/objective-c/EnvoyEngine.mm | 24 +++ mobile/library/swift/BUILD | 27 +++ mobile/library/swift/Envoy.swift | 36 ++++ 11 files changed, 338 insertions(+), 51 deletions(-) create mode 100644 mobile/bazel/swift_static_framework.bzl create mode 100644 mobile/library/objective-c/EnvoyEngine.h create mode 100644 mobile/library/objective-c/EnvoyEngine.mm create mode 100644 mobile/library/swift/BUILD create mode 100644 mobile/library/swift/Envoy.swift diff --git a/mobile/BUILD b/mobile/BUILD index 7ac045436ae6..c48a357b0801 100644 --- a/mobile/BUILD +++ b/mobile/BUILD @@ -6,15 +6,9 @@ load("@io_bazel_rules_kotlin//kotlin/internal:toolchains.bzl", "define_kt_toolch envoy_package() -ios_static_framework( +alias( name = "ios_framework", - hdrs = [ - "//library/objective-c:envoy_framework_headers", - ], - bundle_name = "Envoy", - minimum_os_version = "10.0", - visibility = ["//visibility:public"], - deps = ["//library/objective-c:envoy_objc_interface_lib"], + actual = "//library/swift:ios_framework", ) genrule( diff --git a/mobile/azure-pipelines.yml b/mobile/azure-pipelines.yml index b1a90b6c478f..e412e5732884 100644 --- a/mobile/azure-pipelines.yml +++ b/mobile/azure-pipelines.yml @@ -265,41 +265,41 @@ stages: inputs: artifactName: 'Envoy.framework' targetPath: 'dist/Envoy.framework' - - job: mac_objc_helloworld - dependsOn: mac_dist - timeoutInMinutes: 60 - pool: - vmImage: 'macos-10.14' - steps: - - checkout: self - submodules: true - - script: ./ci/mac_ci_setup.sh - displayName: 'Install dependencies' - - script: mkdir -p dist/Envoy.framework - displayName: 'Create directory for distributable' - - task: DownloadPipelineArtifact@0 - displayName: 'Download Envoy.framework distributable' - inputs: - artifactName: Envoy.framework - targetPath: dist/Envoy.framework - - script: bazel build --config=ios //examples/objective-c/hello_world:app - displayName: 'Build objective-c app' - # Now check that the app actually runs on the simulator. - # This is a non-ideal way to check for liveliness, but works for now. - # First start the iOS simulator. - # Interestingly bazel run does not start the simulator in CI. - # https://github.com/lyft/envoy-mobile/issues/201 for further investigation. - - script: npm install -g ios-sim && ios-sim start --devicetypeid "iPhone-X, 12.2" - displayName: 'Start the iOS simulator' - # Run the app in the background and redirect logs. - - script: bazel run --config=ios //examples/objective-c/hello_world:app &> /tmp/envoy.log & - displayName: 'Run objective-c app' - # Wait for the app to start and get some requests/responses. - - script: sleep 60 - displayName: 'Sleep' - # Check for the sentinel value that shows the app is alive and well. - - script: cat /tmp/envoy.log | grep 'Hello, world!' - displayName: 'Check liveliness' + #- job: mac_objc_helloworld + # dependsOn: mac_dist + # timeoutInMinutes: 60 + # pool: + # vmImage: 'macos-10.14' + # steps: + # - checkout: self + # submodules: true + # - script: ./ci/mac_ci_setup.sh + # displayName: 'Install dependencies' + # - script: mkdir -p dist/Envoy.framework + # displayName: 'Create directory for distributable' + # - task: DownloadPipelineArtifact@0 + # displayName: 'Download Envoy.framework distributable' + # inputs: + # artifactName: Envoy.framework + # targetPath: dist/Envoy.framework + # - script: bazel build --config=ios //examples/objective-c/hello_world:app + # displayName: 'Build objective-c app' + # # Now check that the app actually runs on the simulator. + # # This is a non-ideal way to check for liveliness, but works for now. + # # First start the iOS simulator. + # # Interestingly bazel run does not start the simulator in CI. + # # https://github.com/lyft/envoy-mobile/issues/201 for further investigation. + # - script: npm install -g ios-sim && ios-sim start --devicetypeid "iPhone-X, 12.2" + # displayName: 'Start the iOS simulator' + # # Run the app in the background and redirect logs. + # - script: bazel run --config=ios //examples/objective-c/hello_world:app &> /tmp/envoy.log & + # displayName: 'Run objective-c app' + # # Wait for the app to start and get some requests/responses. + # - script: sleep 60 + # displayName: 'Sleep' + # # Check for the sentinel value that shows the app is alive and well. + # - script: cat /tmp/envoy.log | grep 'Hello, world!' + # displayName: 'Check liveliness' - job: mac_swift_helloworld dependsOn: mac_dist timeoutInMinutes: 60 diff --git a/mobile/bazel/swift_static_framework.bzl b/mobile/bazel/swift_static_framework.bzl new file mode 100644 index 000000000000..99ff1e84d16d --- /dev/null +++ b/mobile/bazel/swift_static_framework.bzl @@ -0,0 +1,183 @@ +""" +This rules creates a fat static framework that can be included later with +static_framework_import +""" + +load("@build_bazel_apple_support//lib:apple_support.bzl", "apple_support") +load("@build_bazel_rules_swift//swift:swift.bzl", "SwiftInfo", "swift_library") + +MINIMUM_IOS_VERSION = "10.0" + +_PLATFORM_TO_SWIFTMODULE = { + "ios_armv7": "arm", + "ios_arm64": "arm64", + "ios_i386": "i386", + "ios_x86_64": "x86_64", +} + +def _zip_binary_arg(module_name, input_file): + return "{module_name}.framework/{module_name}={file_path}".format( + module_name = module_name, + file_path = input_file.path, + ) + +def _zip_swift_arg(module_name, swift_identifier, input_file): + return "{module_name}.framework/Modules/{module_name}.swiftmodule/{swift_identifier}.{ext}={file_path}".format( + module_name = module_name, + swift_identifier = swift_identifier, + ext = input_file.extension, + file_path = input_file.path, + ) + +def _swift_static_framework_impl(ctx): + module_name = ctx.attr.framework_name + fat_file = ctx.outputs.fat_file + + input_archives = [] + input_modules_docs = [] + zip_args = [_zip_binary_arg(module_name, fat_file)] + + for platform, archive in ctx.split_attr.archive.items(): + swiftmodule_identifier = _PLATFORM_TO_SWIFTMODULE[platform] + if not swiftmodule_identifier: + fail("Unhandled platform '{}'".format(platform)) + + swift_info = archive[SwiftInfo] + swiftdoc = swift_info.direct_swiftdocs[0] + swiftmodule = swift_info.direct_swiftmodules[0] + + libraries = archive[CcInfo].linking_context.libraries_to_link + archives = [] + for library in libraries: + archive = library.pic_static_library or library.static_library + if archive: + archives.append(archive) + else: + fail("All linked dependencies must be static") + + platform_archive = ctx.actions.declare_file("{}.{}.a".format(module_name, platform)) + + libtool_args = ["-no_warning_for_no_symbols", "-static", "-syslibroot", "__BAZEL_XCODE_SDKROOT__", "-o", platform_archive.path] + [x.path for x in archives] + apple_support.run( + ctx, + inputs = archives, + outputs = [platform_archive], + mnemonic = "LibtoolLinkedLibraries", + progress_message = "Combining libraries for {} on {}".format(module_name, platform), + executable = ctx.executable._libtool, + arguments = libtool_args, + ) + + input_archives.append(platform_archive) + + input_modules_docs += [swiftdoc, swiftmodule] + zip_args += [ + _zip_swift_arg(module_name, swiftmodule_identifier, swiftdoc), + _zip_swift_arg(module_name, swiftmodule_identifier, swiftmodule), + ] + + ctx.actions.run( + inputs = input_archives, + outputs = [fat_file], + mnemonic = "LipoPlatformLibraries", + progress_message = "Creating fat library for {}".format(module_name), + executable = "lipo", + arguments = ["-create", "-output", fat_file.path] + [x.path for x in input_archives], + ) + + output_file = ctx.outputs.output_file + ctx.actions.run( + inputs = input_modules_docs + [fat_file], + outputs = [output_file], + mnemonic = "CreateFrameworkZip", + progress_message = "Creating framework zip for {}".format(module_name), + executable = ctx.executable._zipper, + arguments = ["c", output_file.path] + zip_args, + ) + + return [ + DefaultInfo( + files = depset([output_file]), + ), + ] + +_swift_static_framework = rule( + attrs = dict( + apple_support.action_required_attrs(), + _libtool = attr.label( + default = "@bazel_tools//tools/objc:libtool", + cfg = "host", + executable = True, + ), + _zipper = attr.label( + default = "@bazel_tools//tools/zip:zipper", + cfg = "host", + executable = True, + ), + archive = attr.label( + mandatory = True, + providers = [ + CcInfo, + SwiftInfo, + ], + cfg = apple_common.multi_arch_split, + ), + framework_name = attr.string(mandatory = True), + minimum_os_version = attr.string(default = MINIMUM_IOS_VERSION), + platform_type = attr.string( + default = str(apple_common.platform_type.ios), + ), + ), + fragments = [ + "apple", + ], + outputs = { + "fat_file": "%{framework_name}.fat", + "output_file": "%{framework_name}.zip", + }, + implementation = _swift_static_framework_impl, +) + +def swift_static_framework( + name, + module_name = None, + srcs = [], + deps = [], + objc_includes = [], + copts = [], + swiftc_inputs = [], + visibility = []): + """Create a static library, and static framework target for a swift module + + Args: + name: The name of the module, the framework's name will be this name + appending Framework so you can depend on this from other modules + srcs: Custom source paths for the swift files + objc_includes: Header files for any objective-c dependencies (required for linking) + copts: Any custom swiftc opts passed through to the swift_library + swiftc_inputs: Any labels that require expansion for copts (would also apply to linkopts) + deps: Any deps the swift_library requires + """ + archive_name = name + "_archive" + module_name = module_name or name + "_framework" + if objc_includes: + locations = ["$(location {})".format(x) for x in objc_includes] + copts = copts + ["-import-objc-header"] + locations + swiftc_inputs = swiftc_inputs + objc_includes + + swift_library( + name = archive_name, + srcs = srcs, + copts = copts, + swiftc_inputs = swiftc_inputs, + module_name = module_name, + visibility = ["//visibility:public"], + deps = deps, + ) + + _swift_static_framework( + name = name, + archive = archive_name, + framework_name = module_name, + visibility = visibility, + ) diff --git a/mobile/dist/BUILD b/mobile/dist/BUILD index d0bc377d5ae8..7d778dad8692 100644 --- a/mobile/dist/BUILD +++ b/mobile/dist/BUILD @@ -13,6 +13,7 @@ envoy_package() aar_import( name = "envoy_mobile_android", aar = "envoy.aar", + visibility = ["//visibility:public"], ) apple_static_framework_import( @@ -22,4 +23,5 @@ apple_static_framework_import( "resolv.9", "c++", ], + visibility = ["//visibility:public"], ) diff --git a/mobile/docs/root/start/examples/hello_world.rst b/mobile/docs/root/start/examples/hello_world.rst index d1877b61c9dc..530d92e9f139 100644 --- a/mobile/docs/root/start/examples/hello_world.rst +++ b/mobile/docs/root/start/examples/hello_world.rst @@ -60,6 +60,11 @@ Open it up, and requests will start flowing! Objective-C ----------- +.. attention:: + + As of `this PR `_ the objective-c demo cannot be built. + We have filed an :issue:`issue 230 <230>` and will fix as expediently as possible. + First, build the :ref:`ios_framework` artifact. Next, run the :repo:`sample app ` using the following Bazel build diff --git a/mobile/examples/objective-c/hello_world/AppDelegate.mm b/mobile/examples/objective-c/hello_world/AppDelegate.mm index d0fe371396e0..5633e61ab828 100644 --- a/mobile/examples/objective-c/hello_world/AppDelegate.mm +++ b/mobile/examples/objective-c/hello_world/AppDelegate.mm @@ -1,5 +1,5 @@ #import "AppDelegate.h" -#import +#import #import #import "ViewController.h" diff --git a/mobile/library/objective-c/BUILD b/mobile/library/objective-c/BUILD index d3f3e490b96f..aee7c976dec6 100644 --- a/mobile/library/objective-c/BUILD +++ b/mobile/library/objective-c/BUILD @@ -1,18 +1,20 @@ licenses(["notice"]) # Apache 2 +exports_files(["EnvoyEngine.h"]) + filegroup( - name = "envoy_framework_headers", - srcs = [ - "Envoy.h", - ], + name = "envoy_engine_hdrs", + srcs = ["EnvoyEngine.h"], visibility = ["//visibility:public"], ) objc_library( - name = "envoy_objc_interface_lib", + name = "envoy_engine_objc_lib", srcs = [ - "Envoy.h", - "Envoy.mm", + "EnvoyEngine.mm", + ], + hdrs = [ + "EnvoyEngine.h", ], copts = ["-std=c++14"], visibility = ["//visibility:public"], diff --git a/mobile/library/objective-c/EnvoyEngine.h b/mobile/library/objective-c/EnvoyEngine.h new file mode 100644 index 000000000000..97e53a48c226 --- /dev/null +++ b/mobile/library/objective-c/EnvoyEngine.h @@ -0,0 +1,14 @@ +#import + +/// Wrapper layer to simplify calling into Envoy's C++ API. +@interface EnvoyEngine : NSObject + +/// Run the Envoy engine with the provided config and log level. This call is synchronous +/// and will not yield. ++ (int)runWithConfig:(NSString *)config; + +/// Run the Envoy engine with the provided config and log level. This call is synchronous +/// and will not yield. ++ (int)runWithConfig:(NSString *)config logLevel:(NSString *)logLevel; + +@end diff --git a/mobile/library/objective-c/EnvoyEngine.mm b/mobile/library/objective-c/EnvoyEngine.mm new file mode 100644 index 000000000000..98b7a9be2ec8 --- /dev/null +++ b/mobile/library/objective-c/EnvoyEngine.mm @@ -0,0 +1,24 @@ +#import "library/objective-c/EnvoyEngine.h" + +#import "library/common/main_interface.h" + +@implementation EnvoyEngine + ++ (int)runWithConfig:(NSString *)config { + return [self runWithConfig:config logLevel:@"info"]; +} + ++ (int)runWithConfig:(NSString *)config logLevel:(NSString *)logLevel { + try { + return run_envoy(config.UTF8String, logLevel.UTF8String); + } catch (NSException *e) { + NSLog(@"Envoy exception: %@", e); + NSDictionary *userInfo = @{@"exception" : e}; + [NSNotificationCenter.defaultCenter postNotificationName:@"EnvoyException" + object:self + userInfo:userInfo]; + return 1; + } +} + +@end diff --git a/mobile/library/swift/BUILD b/mobile/library/swift/BUILD new file mode 100644 index 000000000000..566eb7e33b58 --- /dev/null +++ b/mobile/library/swift/BUILD @@ -0,0 +1,27 @@ +licenses(["notice"]) # Apache 2 + +load("@envoy//bazel:envoy_build_system.bzl", "envoy_package") +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +envoy_package() + +load("//bazel:swift_static_framework.bzl", "swift_static_framework") + +swift_static_framework( + name = "ios_framework", + srcs = ["Envoy.swift"], + module_name = "Envoy", + objc_includes = ["//library/objective-c:EnvoyEngine.h"], + visibility = ["//visibility:public"], + deps = ["//library/objective-c:envoy_engine_objc_lib"], +) + +swift_library( + name = "envoy_swift_lib", + srcs = [ + "Envoy.swift", + ], + module_name = "Envoy", + visibility = ["//visibility:public"], + deps = ["//library/objective-c:envoy_engine_objc_lib"], +) diff --git a/mobile/library/swift/Envoy.swift b/mobile/library/swift/Envoy.swift new file mode 100644 index 000000000000..de9b7cd450f8 --- /dev/null +++ b/mobile/library/swift/Envoy.swift @@ -0,0 +1,36 @@ +import Foundation + +public class Envoy { + private let runner: EnvoyRunner + + public var isRunning: Bool { + return runner.isExecuting + } + + public var isTerminated: Bool { + return runner.isFinished + } + + public init(config: String, logLevel: String) { + runner = EnvoyRunner(config: config, logLevel: logLevel) + runner.start() + } + + public convenience init(config: String) { + self.init(config: config, logLevel: "info") + } + + private class EnvoyRunner: Thread { + let config: String + let logLevel: String + + init(config: String, logLevel: String) { + self.config = config + self.logLevel = logLevel + } + + override func main() { + EnvoyEngine.run(withConfig: config, logLevel: logLevel) + } + } +}