Skip to content

Commit

Permalink
library: migrate public/feature layer of iOS library to Swift
Browse files Browse the repository at this point in the history
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 <mike.schore@gmail.com>
Signed-off-by: JP Simard <jp@jpsim.com>
  • Loading branch information
goaway authored and jpsim committed Nov 28, 2022
1 parent 15db586 commit 073484b
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 51 deletions.
10 changes: 2 additions & 8 deletions mobile/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
70 changes: 35 additions & 35 deletions mobile/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
183 changes: 183 additions & 0 deletions mobile/bazel/swift_static_framework.bzl
Original file line number Diff line number Diff line change
@@ -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,
)
2 changes: 2 additions & 0 deletions mobile/dist/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ envoy_package()
aar_import(
name = "envoy_mobile_android",
aar = "envoy.aar",
visibility = ["//visibility:public"],
)

apple_static_framework_import(
Expand All @@ -22,4 +23,5 @@ apple_static_framework_import(
"resolv.9",
"c++",
],
visibility = ["//visibility:public"],
)
5 changes: 5 additions & 0 deletions mobile/docs/root/start/examples/hello_world.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ Open it up, and requests will start flowing!
Objective-C
-----------

.. attention::

As of `this PR <https://github.com/lyft/envoy-mobile/pull/188>`_ 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 <examples/objective-c/hello_world>` using the following Bazel build
Expand Down
2 changes: 1 addition & 1 deletion mobile/examples/objective-c/hello_world/AppDelegate.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#import "AppDelegate.h"
#import <Envoy/Envoy.h>
#import <Envoy/Envoy-Swift.h>
#import <UIKit/UIKit.h>
#import "ViewController.h"

Expand Down
16 changes: 9 additions & 7 deletions mobile/library/objective-c/BUILD
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down
14 changes: 14 additions & 0 deletions mobile/library/objective-c/EnvoyEngine.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#import <Foundation/Foundation.h>

/// 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
24 changes: 24 additions & 0 deletions mobile/library/objective-c/EnvoyEngine.mm
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 073484b

Please sign in to comment.