Apple Rules - Frameworks

Background

This document describes the usages of framework rules and how to integrate them into application rules. In this document a framework will be defined as an Apple bundle with a .framework extension, used to share code either as a distributable SDK or between applications and extensions within an application bundle.

There are two types of framework bundles, and the difference lies in how the underlying binary will be linked into the final application binary, and how/where the resources will be packaged inside the application bundle:

  • Dynamic Frameworks: These frameworks contain fully linked dylib binaries. Application/Extension/Framework binaries link dynamically against the dylib and will expect it to be present at runtime at a specific location determined by the -rpath linker flag. Dynamic frameworks are packaged into application bundles under the Frameworks directory, preserving the .framework bundle structure, which may include resources.
  • Static Frameworks: These frameworks contain unlinked object archive binaries. Application/Extension/Framework binaries link the archive statically, meaning that the code objects will be embedded in the linked binary. Static frameworks are not packaged into the application bundle. However, any resources that the static framework bundle might contain should be copied into the root of the linked application binary's bundle.

The Apple rules supports two mechanisms to use framework bundles:

  • Framework bundles imported from the workspace.
  • Framework bundles created dynamically during the build.

Frameworks imported from the workspace are useful when integrating 3rd party dependencies that are distributed in precompiled Dynamic or Static frameworks. For imported bundles there are two available rules:

  • apple_dynamic_framework_import for Dynamic Frameworks.
  • apple_static_framework_import for Static Frameworks.

Frameworks created dynamically during the build is the recommended approach for 1st party dependencies. For framework bundles created dynamically during the build, there are two available approaches:

  • ios_framework/tvos_framework for Dynamic Frameworks: This rule is optimized for sharing binary code between application and extension binaries in iOS/tvOS applications. It was not designed to build a distributable Dynamic Framework.
  • ios_static_framework for Static Frameworks: This rule is optimized for distributing precompiled code to be integrated into other build systems. It was not designed for building iOS applications during a bazel build.

NOTE: macOS and watchOS frameworks are not yet supported.

Described Use Cases

ios_framework

A simple ios_application target might look like this:

ios_application(
  name = "MyiOSApp",
  deps = [":MyAppLib"],
)

objc_library(
  name = "MyAppLib",
  srcs = [...],
  deps = [":LargeSharedLib"],
)

objc_library(
  name = "LargeSharedLib",
  srcs = [...],
)

Simple ios_application

If an ios_extension is added to the BUILD graph, it would instead look like this:

ios_application(
  name = "MyiOSApp",
  deps = [":MyAppLib"],
)

ios_extension(
  name = "MyiOSExtension",
  deps = [":MyExtensionLib"],
)

objc_library(
  name = "MyAppLib",
  srcs = [...],
  deps = [":LargeSharedLib"],
)

objc_library(
  name = "MyExtensionLib",
  srcs = [...],
  deps = [":LargeSharedLib"],
)

objc_library(
  name = "LargeSharedLib",
  srcs = [...],
)

ios_application with ios_extension

With this setup, both the application and extension binaries have statically linked the shared library into their binaries, increasing the size of the binary contents of the application bundle. In order to reduce this size, an ios_framework target could be introduced to extract the shared library from the application and extension binaries:

ios_application(
  name = "MyiOSApp",
  frameworks = [":MyiOSFramework"],
  deps = [":MyAppLib"],
)

ios_extension(
  name = "MyiOSExtension",
  frameworks = [":MyiOSFramework"],
  deps = [":MyExtensionLib"],
)

ios_framework(
  name = "MyiOSFramework",
  deps = [":LargeSharedLib"],
)

objc_library(
  name = "MyAppLib",
  srcs = [...],
  deps = [":LargeSharedLib"],
)

objc_library(
  name = "MyExtensionLib",
  srcs = [...],
  deps = [":LargeSharedLib"],
)

objc_library(
  name = "LargeSharedLib",
  srcs = [...],
)

ios_application with ios_framework

Keep in mind that by introducing the ios_framework target, the original dependency graph has not been modified. Therefore ios_framework can be thought of as a tool to manage how code is packaged inside the different binaries present within an iOS application bundle.

apple_dynamic_framework_import and apple_static_framework_import

In order to depend on an imported pre-built static or dynamic framework, you'll need to wrap the files using either the apple_dynamic_framework_import or apple_static_framework_import rule:

apple_static_framework_import(
  name = "MyPrebuiltStaticFramework",
  framework_imports = glob(["MyPrebuiltStaticFramework.framework/**"]),
)

apple_dynamic_framework_import(
  name = "MyPrebuiltDynamicFramework",
  framework_imports = glob(["MyPrebuiltDynamicFramework.framework/**"]),
)

To compile and link against these targets, they need to be added into the deps attribute of a library target:

objc_library(
  name = "MyLibrary",
  srcs = [...],
  deps = [":MyPrebuiltStaticFramework"],
)

objc_library(
  name = "MyOtherLibrary",
  srcs = [...],
  deps = [":MyPrebuiltDynamicFramework"],
)

apple_static_framework_import(
  name = "MyPrebuiltStaticFramework",
  framework_imports = glob(["MyPrebuiltStaticFramework.framework/**"]),
)

apple_dynamic_framework_import(
  name = "MyPrebuiltDynamicFramework",
  framework_imports = glob(["MyPrebuiltDynamicFramework.framework/**"]),
)

apple_static_framework_import targets are linked statically along with any library target that references them. apple_dynamic_framework_import targets on the other hand, are propagated downstream to the top-level bundling rule, and the imported files will be packaged inside the Frameworks directory of the bundle. This way, imported Dynamic Frameworks can be used at runtime by the application:

ios_application(
  name = "MyApplication",
  deps = [
    ":MyLibrary",
    ":MyOtherLibrary",
  ],
)

objc_library(
  name = "MyLibrary",
  srcs = [...],
  deps = [":MyPrebuiltStaticFramework"],
)

objc_library(
  name = "MyOtherLibrary",
  srcs = [...],
  deps = [":MyPrebuiltDynamicFramework"],
)

apple_static_framework_import(
  name = "MyPrebuiltStaticFramework",
  framework_imports = glob(["MyPrebuiltStaticFramework.framework/**"]),
)

apple_dynamic_framework_import(
  name = "MyPrebuiltDynamicFramework",
  framework_imports = glob(["MyPrebuiltDynamicFramework.framework/**"]),
)

Framework imports

Migration

apple_dynamic_framework_import and apple_static_framework_import are Starlark replacement rules for the native objc_framework rule, which is deprecated and slated to be removed in Q1-19. The following is an example of how migration will happen for objc_framework targets for Dynamic Frameworks:

objc_framework(
  name = "MyPrebuiltDynamicFramework",
  framework_imports = glob(["MyPrebuiltDynamicFramework.framework/**"]),
  is_dynamic = 1,
  sdk_dylibs = [...],
  sdk_frameworks = [...],
  weak_sdk_frameworks = [...],
)

apple_dynamic_framework_import(
  name = "MyPrebuildDynamicFramework",
  framework_imports = glob(["MyPrebuiltDynamicFramework.framework/**"]),
)

Notice that the sdk_frameworks, weak_sdk_frameworks and sdk_dylibs were removed. This is because Dynamic Frameworks are already linked, and this information should already be encoded in the binary. If your build starts failing after this change, it means that the import was masking another library that required an extra SDK framework/dylib. That library should be fixed instead.

For Static Frameworks, migration would look like this:

objc_framework(
  name = "MyPrebuiltStaticFramework",
  framework_imports = glob(["MyPrebuiltStaticFramework.framework/**"]),
  sdk_dylibs = [...],
  sdk_frameworks = [...]
  weak_sdk_frameworks = [...],
)

apple_static_framework_import(
  name = "MyPrebuildStaticFramework",
  framework_imports = glob(["MyPrebuiltStaticFramework.framework/**"]),
  sdk_dylibs = [...],
  sdk_frameworks = [...]
  weak_sdk_frameworks = [...],
)