rules_apple icon indicating copy to clipboard operation
rules_apple copied to clipboard

Add ios_bundle rule

Open brentleyjones opened this issue 4 years ago • 23 comments

Note: I haven't tested this rebase of https://github.com/bazelbuild/rules_apple/commit/558ea6a7fa5f2cb15b6bb4278ab12c46b6d11444. I'm just placing this here as a request in #1081.

Resolves #1081.

brentleyjones avatar Feb 22 '21 02:02 brentleyjones

I'll fix the build errors soon

brentleyjones avatar Feb 22 '21 17:02 brentleyjones

@erikkerber Can you provide the BUILD file for EarlGrey? And an example one for a loadable bundle? I know we had a lot of trouble getting it to work correctly.

brentleyjones avatar Mar 02 '21 13:03 brentleyjones

The IPC bundle:

load("@build_bazel_rules_apple//apple:ios.bzl", "ios_bundle")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load("//bazel/rules:constants.bzl", "DEVICE_FAMILIES", "MIN_IOS_VERSION", "TEAM_ID")
load("//bazel/rules:provisioning_profiles.bzl", "provisioning_profile")

ios_bundle(
    name = "Bundle",
    bundle_id = "com.target.Target.EarlGreyIPCBundle",
    bundle_location = "EarlGreyHelperBundles",
    bundle_name = "FlagshipEarlGreyIPCBundle",
    families = DEVICE_FAMILIES,
    frameworks = ["//src/Apps/Shared/EarlGrey:IPCFramework"],
    infoplists = ["Info.plist"],
    minimum_os_version = MIN_IOS_VERSION,
    provisioning_profile = provisioning_profile(
        "Bundle",
        {
            "//:dev_signed": "Test App Wildcard Development",
            "//conditions:default": None,
        },
        bundle_id = "com.target.Target.EarlGreyIPCBundle",
        team_id = TEAM_ID,
    ),
    version = "//src/Apps/Flagship:Version",
    visibility = ["//src/Apps/Flagship:__pkg__"],
    deps = [":Bundle.__tgt__.library"],
)

swift_library(
    name = "Bundle.__tgt__.library",
    srcs = glob(["Implementations/**/*.swift"]),
    module_name = "FlagshipEarlGreyIPC",
    deps = [
        ":Interfaces",
        "@com_github_alisoftware_ohhttpstubs//:OHHTTPStubs",
        "@com_github_alisoftware_ohhttpstubs//:OHHTTPStubsSwift",
        "@com_github_google_earlgrey//:EarlGreyIPC",
    ],
)

swift_library(
    name = "Interfaces",
    srcs = glob(["Interfaces/**/*.swift"]),
    module_name = "FlagshipEarlGreyIPCInterfaces",
    # TODO: Limit visibility to UI tests
    visibility = ["//visibility:public"],
)

EarlGrey:

load("@build_bazel_rules_apple//apple:ios.bzl", "ios_framework")

# This is the framework that IPC bundles will link against.
# It mirrors the framework that is bundled in the app under test, but has `bundle_only = False`.
ios_framework(
    name = "IPCFramework",
    bundle_id = "com.google.earlgrey.AppFramework",
    bundle_name = "AppFramework",
    families = [
        "iphone",
        "ipad",
    ],
    infoplists = ["@com_github_google_earlgrey//:AppFramework/Info.plist"],
    minimum_os_version = "10.0",
    # TODO: This should be limited to IPC bundles only
    visibility = ["//visibility:public"],
    deps = ["@com_github_google_earlgrey//:EarlGreyIPC"],
)

erikkerber avatar Mar 02 '21 18:03 erikkerber

EarlGrey itself:

load("@build_bazel_rules_apple//apple:ios.bzl", "ios_framework")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load("@rules_cc//cc:defs.bzl", "objc_library")

# This project is super sloppy, they allow includes from all of the libraries
ALL_INCLUDES = [
    "AppFramework/Action",
    "AppFramework/Additions",
    "AppFramework/Assertion",
    "AppFramework/AutomationSetup",
    "AppFramework/Config",
    "AppFramework/Core",
    "AppFramework/Delegate",
    "AppFramework/DistantObject",
    "AppFramework/EarlGreyApp",
    "AppFramework/Error",
    "AppFramework/Event",
    "AppFramework/IdlingResources",
    "AppFramework/Keyboard",
    "AppFramework/Matcher",
    "AppFramework/Synchronization",
    "CommonLib/",
    "CommonLib/Additions",
    "CommonLib/Assertion",
    "CommonLib/Config",
    "CommonLib/DistantObject",
    "CommonLib/Error",
    "CommonLib/Event",
    "CommonLib/Exceptions",
    "CommonLib/Matcher",
    "CommonLib/Provider",
    "TestLib/AlertHandling",
    "TestLib/AppleInternals",
    "TestLib/Assertion",
    "TestLib/Condition",
    "TestLib/Config",
    "TestLib/DistantObject",
    "TestLib/EarlGreyImpl",
    "TestLib/Exception",
    "TestLib/Execution",
    "TestLib/XCTestCase",
    "UILib",
    "UILib/Additions",
    "UILib/Provider",
    "UILib/Traversal",
    "UILib/Visibility",
]

APPFRAMEWORK_SRCS = glob(["AppFramework/**/*.m"])
COMMONLIB_SRCS = glob(["CommonLib/**/*.m"])
TESTLIB_SRCS = glob(["TestLib/**/*.m"]) + [
    "AppFramework/Action/GREYActionsShorthand.m",
    "AppFramework/Matcher/GREYMatchersShorthand.m",
]
UILIB_SRCS = glob(["UILib/**/*.m"])

objc_library(
    name = "TestLib",
    copts = ["-DSLOPPY_IMPORTS"],
    module_name = "TestLib",
    includes = ALL_INCLUDES,
    hdrs = glob([
        "AppFramework/**/*.h",
        "CommonLib/**/*.h",
        "TestLib/**/*.h",
        "UILib/**/*.h",
    ], exclude = ["TestLib/Swift/*.h"]),
    srcs = TESTLIB_SRCS + COMMONLIB_SRCS,
    deps = [
        "@com_github_google_edistantobject//:eDistantObject",
    ],
)

# This is the target that will actually be imported into our UI test targets
genrule(
    name = "Imports",
    outs = ["Imports.swift"],
    cmd = """cat <<-END > $@
@_exported import TestLib
END
"""
)

swift_library(
    name = "EarlGrey",
    srcs = glob(["TestLib/Swift/**/*.swift"]) + [":Imports"],
    visibility = ["//visibility:public"],
    deps = [
        ":TestLib",
    ],
)

# This target will get linked into our loadable IPC bundles, via a `AppFramework`
objc_library(
    name = "EarlGreyIPC",
    copts = ["-DSLOPPY_IMPORTS"],
    hdrs = glob([
        "AppFramework/**/*.h",
        "CommonLib/**/*.h",
        "UILib/**/*.h",
    ]),
    includes = ALL_INCLUDES,
    srcs = APPFRAMEWORK_SRCS + COMMONLIB_SRCS + UILIB_SRCS,
    module_name = "EarlGreyIPC",
    sdk_frameworks = [
        "CoreData",
        "CoreGraphics",
        "IOKit",
        "QuartzCore",
        "WebKit",
    ],
    visibility = ["//visibility:public"],
    deps = [
        "@com_github_facebook_fishhook//:fishhook",
        "@com_github_google_edistantobject//:eDistantObject",
    ],
)

# This target will get bundle into the app, only under test
ios_framework(
    name = "AppFramework",
    bundle_id = "com.google.earlgrey.AppFramework",
    bundle_only = True,
    families = ["iphone", "ipad"],
    infoplists = ["AppFramework/Info.plist"],
    minimum_os_version = "10.0",
    visibility = ["//visibility:public"],
    deps = [":EarlGreyIPC"],
)

# This needs to be public, to allow building the loadable bundle's AppFramework.framework
# (which is done outside of this repo, because of )
exports_files(["AppFramework/Info.plist"])

erikkerber avatar Mar 02 '21 19:03 erikkerber

@ajanuar Hopefully the above will set you in the right direction. I'll try to fix this PR soon, just been bogged down.

brentleyjones avatar Mar 02 '21 19:03 brentleyjones

thanks @erikkerber ! one more question, how to use Bundle in ios_ui_test?

ajanuar avatar Mar 03 '21 00:03 ajanuar

Bundle is in the bundles for the app under test. Interfaces is what you depend on in the UI test in order to invoke code in the bundle.

brentleyjones avatar Mar 03 '21 03:03 brentleyjones

Yes. In your ios_application:

ios_application(
  ...
  bundles = select({
    "//:dbg": ["//src/EarlGreyIPC:Bundle"],
    "//conditions:default": [],
   })
   ...
}

And like Brentley said, Interfaces will be in your deps for your UI test target/BUILD file.

erikkerber avatar Mar 03 '21 04:03 erikkerber

Thanks! Will try today.

ajanuar avatar Mar 03 '21 04:03 ajanuar

Build is green. For this to get over the finish line it needs some tests. @erikkerber (or a delegate) or @ajanuar, could either of you contribute some?

brentleyjones avatar Mar 04 '21 14:03 brentleyjones

I can run our EarlGreyTest now, thanks all. And yes, I can help to add some tests.

ajanuar avatar Mar 06 '21 03:03 ajanuar

Hello guys,

I have case that need to create ios_bundle binary (type=loadable_bundle) with linker flags (-bundle_loader path/to/app/exe_bin).

if I do this, when I run EarlGreyTests (bazel test :UITest), it will detect there will be duplicate symbols from two different binaries (EarlGreyBundle & App) & App will be crash.

Is there any way to avoid this?

objc_library(
  name = "X",
  deps = ["Y", "Z"]
)

ios_bundle(
  name = "Bundle",
  deps = ["X"],
 ...
)

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

ios_ui_test(
  name = "UITest",
  test_host = ":App",
  deps = ["TestLib"]
)

objc_library(
  name = "AppLib",
  srcs = [...],
  deps = ["Y", "Z"]
)

ios_application(
  name = "App",
  deps = ["AppLib"],
  bundles = ["Bundle"],
  ...
)

Note: For comparison, in BUCK there is rule to create binary (https://buck.build/rule/apple_binary.html) which allow to provide srcs and linker_flags.

Example in BUCK

apple_binary(
  name = "LoadableBundle",
  srcs = [...],
  linker_flags = [
    "-bundle_loader",
    "/path/to/app/exe"
  ]
)

ajanuar avatar Mar 11 '21 14:03 ajanuar

Can you show an error? We never had duplicate symbol errors.

brentleyjones avatar Mar 11 '21 14:03 brentleyjones

An example:

Mar 11 22:17:09 Alberts-MacBook-Pro-2 SandboxApp[87775]: objc[87775]: Class CheckmarkItemCell is implemented in both /Users/albertjanuar/Library/Developer/CoreSimulator/Devices/FC18A482-CAF8-4D84-9C59-2438D3227792/data/Containers/Bundle/Application/BB27D7F3-C937-4ABF-BE04-D91B156F2A6D/SandboxApp.app/SandboxApp (0x10d1459d0) and /Users/albertjanuar/Library/Developer/CoreSimulator/Devices/FC18A482-CAF8-4D84-9C59-2438D3227792/data/Containers/Bundle/Application/BB27D7F3-C937-4ABF-BE04-D91B156F2A6D/SandboxApp.app/EarlGreyHelperBundles/CulinaryEarlGreyHelper.bundle/CulinaryEarlGreyHelper (0x14e07cb10). One of the two will be used. Which one is undefined.

It lists all public headers from Y & Z. Is it because objc_library's output a fully linked static library?

ajanuar avatar Mar 11 '21 15:03 ajanuar

I wouldn't expect something called CheckmarkItemCell to be a symbol in your EarlGrey bundle. Make sure you don't have common dependencies between your app and the CulinaryEarlGreyHelper?

erikkerber avatar Mar 11 '21 22:03 erikkerber

Hi @erikkerber @brentleyjones I just want to clarify what @ajanuar meant. Maybe this is different case by case (or maybe OOT, tied to specific earlgrey 2 usage).

In earlgrey, the helper bundle act as an extension of .app let's use https://github.com/google/EarlGrey/blob/earlgrey2/docs/setup.md and https://github.com/google/eDistantObject/blob/master/docs/setup.md#where-to-write-code as a sample.

From what I learn from the docs, FooClass will be linked only to the helper bundle (host) binary. In our case, FooClass also import Culinary module because it needs access to the module, while .app also linked to the Culinary module. (we used modular)

Thus, there will be 2 duplicate symbol in the .app and helper bundle binary. The duplicate error cause singleton classes not properly initialized.

Does it mean that we have to split shared header between Culinary and it's dependencies module too to be used by helper bundle? Or maybe just us that is wrong using it 😄

hendych avatar Mar 12 '21 10:03 hendych

We didn't have something like Culinary depended on by helper bundle binary. The bundle only provided interfaces that allowed communication with the app, the app had all of the dependencies.

This rule could be fixed to include the -bundle_loader, but it created a unique circular dependency that I didn't have time to go into. I would look at how tests and test_host work, and look at how the custom bundle location works with this PR, and determine a way to get it to behave more like tests (making the bundle "depend" on a certain app), instead of the model I followed which has the app depend on the bundle.

brentleyjones avatar Mar 12 '21 14:03 brentleyjones

We didn't have something like Culinary depended on by helper bundle binary. The bundle only provided interfaces that allowed communication with the app, the app had all of the dependencies.

This rule could be fixed to include the -bundle_loader, but it created a unique circular dependency that I didn't have time to go into. I would look at how tests and test_host work, and look at how the custom bundle location works with this PR, and determine a way to get it to behave more like tests (making the bundle "depend" on a certain app), instead of the model I followed which has the app depend on the bundle.

Noted on that. I understand better on how to use it. Thank you by the way!

hendych avatar Mar 12 '21 14:03 hendych

We didn't have something like Culinary depended on by helper bundle binary. The bundle only provided interfaces that allowed communication with the app, the app had all of the dependencies.

This rule could be fixed to include the -bundle_loader, but it created a unique circular dependency that I didn't have time to go into. I would look at how tests and test_host work, and look at how the custom bundle location works with this PR, and determine a way to get it to behave more like tests (making the bundle "depend" on a certain app), instead of the model I followed which has the app depend on the bundle.

Is the idea to add new parameter to ios_ui_test like test_host_bundles, and copy those bundles when the test is being bundled? https://github.com/bazelbuild/rules_apple/blob/d49933a3a067f34ed94ca8ab09316450ffa1a455/apple/internal/testing/apple_test_rule_support.bzl#L149-L150

ajanuar avatar Mar 15 '21 09:03 ajanuar

ios_unit_test has a test_host attribute that causes the test host to be the -bundle_loader for the unit test. I was saying to copy that logic, to allow a bundle to have a -bundle_loader. A bundle having a bundle loader doesn't depend on UI tests or unit test, so I wouldn't be adding fields to any test rules.

brentleyjones avatar Mar 15 '21 12:03 brentleyjones

Hi @brentleyjones , let me clarify my understanding, these rules below are in circular dependencies.

objc_library(
  name = "X",
)

ios_bundle(
  name = "Bundle",
  deps = [":X"],
  bundle_location = "EarlGreyHelperBundles",
  bundle_loader = ":App",
  ...
)

objc_library(
  name = "UITestLib",
)

ios_ui_test(
  name = "UITest",
  deps = ["UITestLib"],
  test_host = ":App",
)

objc_library(
  name = "AppLib",
)

ios_application(
  name = "App",
  deps = [":AppLib"],
  bundles = [":Bundle"], # We can't do this because of cyclic.
)

Assume we can use the ios_unit_test logic, then can we write these rules below?

objc_library(
  name = "X",
)

ios_bundle(
  name = "Bundle",
  deps = [":X"],
  bundle_location = "EarlGreyHelperBundles",
  bundle_loader = ":App",
  ...
)

objc_library(
  name = "UITestLib",
)

ios_ui_test(
  name = "UITest",
  deps = ["UITestLib"],
  test_host = ":App",
)

objc_library(
  name = "AppLib",
)

ios_application(
  name = "App",
  deps = [":AppLib"],
)

I am available to help if you can guide me steps to enable this. thanks!

ajanuar avatar Mar 17 '21 10:03 ajanuar

Yes, that is the correct understanding.

brentleyjones avatar Mar 17 '21 12:03 brentleyjones

Hi @brentleyjones , sharing a little update. we've successfully built ios bundle with workaround for our EarlGrey test case. This won't be ideal, but it runs perfectly for us.

objc_library(
  name = "X",
)

ios_bundle(
  name = "Bundle",
  deps = [":X"],
  bundle_location = "EarlGreyHelperBundles",
  bundle_loader = ":FakeApp", #workaround
  ...
)

objc_library(
  name = "UITestLib",
)

ios_ui_test(
  name = "UITest",
  deps = ["UITestLib"],
  test_host = ":App",
)

objc_library(
  name = "AppLib",
)

ios_application(
  name = "FakeApp",
  deps = [":AppLib"],
)

ios_application(
  name = "App",
  deps = [":AppLib"],
  bundles = [":Bundle"], # We can't do this because of cyclic.
)

I also create PR to add test https://github.com/bazelbuild/rules_apple/pull/1108, not sure these tests are sufficient. Thanks!

ajanuar avatar Mar 22 '21 05:03 ajanuar

Hi guys, may i know is there any chance to merge this PR into master branch?

dwirandyh avatar Apr 10 '23 07:04 dwirandyh

If anyone wants an ios_bundle rule, feel free to open a PR that adds it. It needs to address my concerns about the bundle loader though. I don't have plans to update and land this PR.

brentleyjones avatar Sep 27 '23 14:09 brentleyjones