From 0bfb9bd1caecf7d2d9e4d2503e92c7d1a932d028 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Wed, 29 Mar 2017 14:42:44 -0700 Subject: [PATCH] Initial commit of Apple rules in Skylark. --- .gitignore | 2 + AUTHORS | 9 + CONTRIBUTING.md | 27 + CONTRIBUTORS | 15 + LICENSE | 202 +++ README.md | 1203 +++++++++++++++++ WORKSPACE | 1 + apple/BUILD | 31 + apple/bundling/BUILD | 40 + apple/bundling/README.md | 16 + apple/bundling/apple_bundling_aspect.bzl | 287 ++++ apple/bundling/binary_support.bzl | 131 ++ apple/bundling/bundler.bzl | 851 ++++++++++++ apple/bundling/bundler.py | 219 +++ apple/bundling/bundler_unittest.py | 298 ++++ apple/bundling/bundling_support.bzl | 248 ++++ apple/bundling/codesigning_support.bzl | 173 +++ apple/bundling/dSYM-Info.plist.template | 20 + apple/bundling/dsym_actions.bzl | 81 ++ apple/bundling/entitlements.bzl | 402 ++++++ apple/bundling/file_actions.bzl | 55 + apple/bundling/file_support.bzl | 41 + apple/bundling/ios_rules.bzl | 290 ++++ apple/bundling/mock_support.bzl | 44 + apple/bundling/modulemap_actions.bzl | 101 ++ apple/bundling/platform_support.bzl | 159 +++ apple/bundling/plist_actions.bzl | 274 ++++ apple/bundling/plist_support.bzl | 78 ++ apple/bundling/plisttool.py | 451 ++++++ apple/bundling/plisttool_unittest.py | 515 +++++++ apple/bundling/process_and_sign.sh.template | 49 + apple/bundling/product_actions.bzl | 122 ++ apple/bundling/product_support.bzl | 166 +++ apple/bundling/provider_support.bzl | 60 + apple/bundling/resource_actions.bzl | 636 +++++++++ apple/bundling/resource_support.bzl | 64 + apple/bundling/rule_attributes.bzl | 268 ++++ apple/bundling/run_actions.bzl | 57 + apple/bundling/swift_actions.bzl | 68 + apple/bundling/swift_support.bzl | 42 + apple/bundling/test_support.bzl | 82 ++ apple/bundling/tvos_rules.bzl | 168 +++ apple/bundling/watchos_rules.bzl | 163 +++ apple/ios.bzl | 250 ++++ apple/providers.bzl | 204 +++ apple/resources.bzl | 28 + apple/tvos.bzl | 164 +++ apple/utils.bzl | 373 +++++ apple/watchos.bzl | 164 +++ examples/ios/HelloWorld/BUILD | 21 + examples/ios/HelloWorld/Info.plist | 36 + .../ios/HelloWorld/Resources/Main.storyboard | 42 + examples/ios/HelloWorld/Sources/AppDelegate.h | 21 + examples/ios/HelloWorld/Sources/AppDelegate.m | 24 + examples/ios/HelloWorld/Sources/main.m | 23 + test/BUILD | 67 + test/apple_shell_testrunner.sh | 81 ++ test/apple_shell_testutils.sh | 495 +++++++ test/configurations.bzl | 58 + test/ios_application_resources_test.sh | 403 ++++++ test/ios_application_test.sh | 532 ++++++++ test/ios_extension_test.sh | 493 +++++++ test/run_integration_tests.sh | 39 + test/test_rules.bzl | 88 ++ test/testdata/binaries/BUILD | 12 + test/testdata/provisioning/BUILD | 5 + .../integration_testing.mobileprovision | 65 + test/testdata/resources/BUILD | 169 +++ .../app_icon.appiconset/Contents.json | 86 ++ .../app_icon.appiconset/app_icon_167pt.png | Bin 0 -> 14792 bytes .../app_icon.appiconset/app_icon_29pt.png | Bin 0 -> 15317 bytes .../app_icon.appiconset/app_icon_29pt_2x.png | Bin 0 -> 14596 bytes .../app_icon.appiconset/app_icon_29pt_3x.png | Bin 0 -> 14652 bytes .../app_icon.appiconset/app_icon_40pt.png | Bin 0 -> 15326 bytes .../app_icon.appiconset/app_icon_40pt_2x.png | Bin 0 -> 14640 bytes .../app_icon.appiconset/app_icon_40pt_3x.png | Bin 0 -> 14722 bytes .../app_icon.appiconset/app_icon_60pt_3x.png | Bin 0 -> 14877 bytes .../app_icon.appiconset/app_icon_76pt.png | Bin 0 -> 14634 bytes .../app_icon.appiconset/app_icon_76pt_2x.png | Bin 0 -> 14810 bytes .../assets_ios.xcassets/Contents.json | 6 + .../star_ipad.imageset/Contents.json | 18 + .../star_ipad.imageset/star.png | Bin 0 -> 802 bytes .../star_ipad.imageset/star_2x.png | Bin 0 -> 1882 bytes .../star_iphone.imageset/Contents.json | 23 + .../star_iphone.imageset/star.png | Bin 0 -> 802 bytes .../star_iphone.imageset/star_2x.png | Bin 0 -> 1882 bytes .../star_iphone.imageset/star_3x.png | Bin 0 -> 2977 bytes .../star_universal.imageset/Contents.json | 23 + .../star_universal.imageset/star.png | Bin 0 -> 802 bytes .../star_universal.imageset/star_2x.png | Bin 0 -> 1882 bytes .../star_universal.imageset/star_3x.png | Bin 0 -> 2977 bytes .../assets_tvos.xcassets/Contents.json | 6 + .../star.imageset/Contents.json | 13 + .../star.imageset/star.png | Bin 0 -> 802 bytes .../assets_watchos.xcassets/Contents.json | 6 + .../star.imageset/Contents.json | 20 + .../star.imageset/star_2x.png | Bin 0 -> 1882 bytes .../resources/basic.bundle/basic_bundle.txt | 1 + .../resources/it.lproj/localized.strings | 1 + .../testdata/resources/it.lproj/localized.txt | 1 + .../it.lproj/storyboard_ios.storyboard | 26 + test/testdata/resources/it.lproj/view_ios.xib | 16 + .../launch_image.launchimage/Contents.json | 111 ++ .../launch_image_1024x1366pt_2x.png | Bin 0 -> 36938 bytes .../launch_image_1024x768pt.png | Bin 0 -> 17893 bytes .../launch_image_1024x768pt_2x.png | Bin 0 -> 27212 bytes .../launch_image_1366x1024pt_2x.png | Bin 0 -> 35688 bytes .../launch_image_320x480pt_2x.png | Bin 0 -> 17532 bytes .../launch_image_320x568pt_2x.png | Bin 0 -> 18081 bytes .../launch_image_375x667pt_2x.png | Bin 0 -> 19292 bytes .../launch_image_414x736pt_3x.png | Bin 0 -> 25562 bytes .../launch_image_736x414pt_3x.png | Bin 0 -> 25409 bytes .../launch_image_768x1024pt.png | Bin 0 -> 18241 bytes .../launch_image_768x1024pt_2x.png | Bin 0 -> 28357 bytes .../resources/launch_screen_ios.storyboard | 27 + test/testdata/resources/nonlocalized.strings | 1 + .../resources/nonlocalized_resource.txt | 2 + .../resources/settings_ios.bundle/Root.plist | 25 + .../settings_ios.bundle/it.lproj/Root.strings | 3 + test/testdata/resources/star.atlas/star.png | Bin 0 -> 802 bytes .../sticker_pack_ios.xcstickers/Contents.json | 6 + .../app_icon.stickersiconset/Contents.json | 85 ++ .../app_icon_1024x768pt.png | Bin 0 -> 383 bytes .../app_icon_27x20pt_2x.png | Bin 0 -> 278 bytes .../app_icon_27x20pt_3x.png | Bin 0 -> 280 bytes .../app_icon_29pt_2x.png | Bin 0 -> 279 bytes .../app_icon_29pt_3x.png | Bin 0 -> 283 bytes .../app_icon_32x24pt_2x.png | Bin 0 -> 279 bytes .../app_icon_32x24pt_3x.png | Bin 0 -> 282 bytes .../app_icon_60x45pt_2x.png | Bin 0 -> 285 bytes .../app_icon_60x45pt_3x.png | Bin 0 -> 291 bytes .../app_icon_67x50pt_2x.png | Bin 0 -> 287 bytes .../app_icon_74x55pt_2x.png | Bin 0 -> 290 bytes .../sticker_pack.stickerpack/Contents.json | 17 + .../sequence.stickersequence/Contents.json | 22 + .../sequence_100pt_1.png | Bin 0 -> 285 bytes .../sequence_100pt_2.png | Bin 0 -> 285 bytes .../sequence_100pt_3.png | Bin 0 -> 285 bytes .../sticker.sticker/Contents.json | 9 + .../sticker.sticker/sticker_100pt.png | Bin 0 -> 285 bytes .../resources/storyboard_ios.storyboard | 26 + test/testdata/resources/structured/nested.txt | 2 + .../contents | 4 + .../.xccurrentversion | 8 + .../v1.xcdatamodel/contents | 4 + .../v2.xcdatamodel/contents | 4 + test/testdata/resources/view_ios.xib | 16 + test/tvos_application_test.sh | 215 +++ test/tvos_extension_test.sh | 170 +++ test/unittest.bash | 801 +++++++++++ test/watchos_application_test.sh | 262 ++++ 151 files changed, 14132 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS create mode 100644 LICENSE create mode 100644 README.md create mode 100644 WORKSPACE create mode 100644 apple/BUILD create mode 100644 apple/bundling/BUILD create mode 100644 apple/bundling/README.md create mode 100644 apple/bundling/apple_bundling_aspect.bzl create mode 100644 apple/bundling/binary_support.bzl create mode 100644 apple/bundling/bundler.bzl create mode 100644 apple/bundling/bundler.py create mode 100644 apple/bundling/bundler_unittest.py create mode 100644 apple/bundling/bundling_support.bzl create mode 100644 apple/bundling/codesigning_support.bzl create mode 100644 apple/bundling/dSYM-Info.plist.template create mode 100644 apple/bundling/dsym_actions.bzl create mode 100644 apple/bundling/entitlements.bzl create mode 100644 apple/bundling/file_actions.bzl create mode 100644 apple/bundling/file_support.bzl create mode 100644 apple/bundling/ios_rules.bzl create mode 100644 apple/bundling/mock_support.bzl create mode 100644 apple/bundling/modulemap_actions.bzl create mode 100644 apple/bundling/platform_support.bzl create mode 100644 apple/bundling/plist_actions.bzl create mode 100644 apple/bundling/plist_support.bzl create mode 100644 apple/bundling/plisttool.py create mode 100644 apple/bundling/plisttool_unittest.py create mode 100644 apple/bundling/process_and_sign.sh.template create mode 100644 apple/bundling/product_actions.bzl create mode 100644 apple/bundling/product_support.bzl create mode 100644 apple/bundling/provider_support.bzl create mode 100644 apple/bundling/resource_actions.bzl create mode 100644 apple/bundling/resource_support.bzl create mode 100644 apple/bundling/rule_attributes.bzl create mode 100644 apple/bundling/run_actions.bzl create mode 100644 apple/bundling/swift_actions.bzl create mode 100644 apple/bundling/swift_support.bzl create mode 100644 apple/bundling/test_support.bzl create mode 100644 apple/bundling/tvos_rules.bzl create mode 100644 apple/bundling/watchos_rules.bzl create mode 100644 apple/ios.bzl create mode 100644 apple/providers.bzl create mode 100644 apple/resources.bzl create mode 100644 apple/tvos.bzl create mode 100644 apple/utils.bzl create mode 100644 apple/watchos.bzl create mode 100644 examples/ios/HelloWorld/BUILD create mode 100644 examples/ios/HelloWorld/Info.plist create mode 100644 examples/ios/HelloWorld/Resources/Main.storyboard create mode 100644 examples/ios/HelloWorld/Sources/AppDelegate.h create mode 100644 examples/ios/HelloWorld/Sources/AppDelegate.m create mode 100644 examples/ios/HelloWorld/Sources/main.m create mode 100644 test/BUILD create mode 100755 test/apple_shell_testrunner.sh create mode 100755 test/apple_shell_testutils.sh create mode 100644 test/configurations.bzl create mode 100755 test/ios_application_resources_test.sh create mode 100755 test/ios_application_test.sh create mode 100755 test/ios_extension_test.sh create mode 100755 test/run_integration_tests.sh create mode 100644 test/test_rules.bzl create mode 100644 test/testdata/binaries/BUILD create mode 100644 test/testdata/provisioning/BUILD create mode 100644 test/testdata/provisioning/integration_testing.mobileprovision create mode 100644 test/testdata/resources/BUILD create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/Contents.json create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_167pt.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_29pt.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_29pt_2x.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_29pt_3x.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_40pt.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_40pt_2x.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_40pt_3x.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_60pt_3x.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_76pt.png create mode 100644 test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_76pt_2x.png create mode 100644 test/testdata/resources/assets_ios.xcassets/Contents.json create mode 100644 test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/Contents.json create mode 100644 test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/star.png create mode 100644 test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/star_2x.png create mode 100644 test/testdata/resources/assets_ios.xcassets/star_iphone.imageset/Contents.json create mode 100644 test/testdata/resources/assets_ios.xcassets/star_iphone.imageset/star.png create mode 100644 test/testdata/resources/assets_ios.xcassets/star_iphone.imageset/star_2x.png create mode 100644 test/testdata/resources/assets_ios.xcassets/star_iphone.imageset/star_3x.png create mode 100644 test/testdata/resources/assets_ios.xcassets/star_universal.imageset/Contents.json create mode 100644 test/testdata/resources/assets_ios.xcassets/star_universal.imageset/star.png create mode 100644 test/testdata/resources/assets_ios.xcassets/star_universal.imageset/star_2x.png create mode 100644 test/testdata/resources/assets_ios.xcassets/star_universal.imageset/star_3x.png create mode 100644 test/testdata/resources/assets_tvos.xcassets/Contents.json create mode 100644 test/testdata/resources/assets_tvos.xcassets/star.imageset/Contents.json create mode 100644 test/testdata/resources/assets_tvos.xcassets/star.imageset/star.png create mode 100644 test/testdata/resources/assets_watchos.xcassets/Contents.json create mode 100644 test/testdata/resources/assets_watchos.xcassets/star.imageset/Contents.json create mode 100644 test/testdata/resources/assets_watchos.xcassets/star.imageset/star_2x.png create mode 100644 test/testdata/resources/basic.bundle/basic_bundle.txt create mode 100644 test/testdata/resources/it.lproj/localized.strings create mode 100644 test/testdata/resources/it.lproj/localized.txt create mode 100644 test/testdata/resources/it.lproj/storyboard_ios.storyboard create mode 100644 test/testdata/resources/it.lproj/view_ios.xib create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/Contents.json create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1024x1366pt_2x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1024x768pt.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1024x768pt_2x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1366x1024pt_2x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_320x480pt_2x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_320x568pt_2x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_375x667pt_2x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_414x736pt_3x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_736x414pt_3x.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_768x1024pt.png create mode 100644 test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_768x1024pt_2x.png create mode 100644 test/testdata/resources/launch_screen_ios.storyboard create mode 100644 test/testdata/resources/nonlocalized.strings create mode 100644 test/testdata/resources/nonlocalized_resource.txt create mode 100644 test/testdata/resources/settings_ios.bundle/Root.plist create mode 100644 test/testdata/resources/settings_ios.bundle/it.lproj/Root.strings create mode 100644 test/testdata/resources/star.atlas/star.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/Contents.json create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/Contents.json create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_1024x768pt.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_27x20pt_2x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_27x20pt_3x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_29pt_2x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_29pt_3x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_32x24pt_2x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_32x24pt_3x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_60x45pt_2x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_60x45pt_3x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_67x50pt_2x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_74x55pt_2x.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/Contents.json create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/Contents.json create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/sequence_100pt_1.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/sequence_100pt_2.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/sequence_100pt_3.png create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sticker.sticker/Contents.json create mode 100644 test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sticker.sticker/sticker_100pt.png create mode 100644 test/testdata/resources/storyboard_ios.storyboard create mode 100644 test/testdata/resources/structured/nested.txt create mode 100644 test/testdata/resources/unversioned_datamodel.xcdatamodel/contents create mode 100644 test/testdata/resources/versioned_datamodel.xcdatamodeld/.xccurrentversion create mode 100644 test/testdata/resources/versioned_datamodel.xcdatamodeld/v1.xcdatamodel/contents create mode 100644 test/testdata/resources/versioned_datamodel.xcdatamodeld/v2.xcdatamodel/contents create mode 100644 test/testdata/resources/view_ios.xib create mode 100755 test/tvos_application_test.sh create mode 100755 test/tvos_extension_test.sh create mode 100755 test/unittest.bash create mode 100755 test/watchos_application_test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..197abdfa42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/bazel-* + diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000000..8f95963acc --- /dev/null +++ b/AUTHORS @@ -0,0 +1,9 @@ +# This the official list of Bazel authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as: +# Name or Organization +# The email address is not required for organizations. + +Google Inc. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..9d3d1aaf8a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +Want to contribute? Great! First, read this page (including the small print at the end). + +### Before you contribute +**Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) +(CLA)**, which you can do online. + +The CLA is necessary mainly because you own the copyright to your changes, +even after your contribution becomes part of our codebase, so we need your +permission to use and distribute your code. We also need to be sure of +various other things — for instance that you'll tell us if you know that +your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch +with us first. Use the issue tracker to explain your idea so we can help and +possibly guide you. + +### Code reviews and other contributions. +**All submissions, including submissions by project members, require review.** +Please follow the instructions in [the contributors documentation](http://bazel.io/contributing.html). + +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the +[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000000..ce7a21a812 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,15 @@ +# People who have agreed to one of the CLAs and can contribute patches. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# https://developers.google.com/open-source/cla/individual +# https://developers.google.com/open-source/cla/corporate +# +# Names should be added to this file as: +# Name + +Christopher Parsons +Dmitry Shevchenko +Sergio Campamá +Tony Allevato diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..976def276a --- /dev/null +++ b/README.md @@ -0,0 +1,1203 @@ +# Apple Rules + +> :warning: **NOTE**: At the time of this writing, the most recent Bazel +> release is **0.4.5.** These rules are *not* compatible with that release; +> they are only compatible with Bazel at **master**. Until the next release of +> Bazel, you will need to +> [build Bazel from source](https://bazel.build/versions/master/docs/install-compile-source.html) +> if you wish to use them. + +This repository contains rules for [Bazel](https://bazel.build) that can be +used to bundle applications for Apple platforms. They replace the bundling +rules defined in Bazel itself (such as `ios_application`, `ios_extension`, and +`apple_watch2_extension`). + +These rules handle the linking and bundling of applications and extensions +(that is, the formation of an `.app` with an executable and resources, +archived in an `.ipa`). Compilation is still performed by the existing +[`objc_library` rule](https://bazel.build/versions/master/docs/be/objective-c.html#objc_library) +in Bazel; to link those dependencies, these bundling rules use Bazel's +[`apple_binary` rule](https://bazel.build/versions/master/docs/be/objective-c.html#apple_binary) +under the hood. + +## Rules + +* [ios_application](#ios_application) +* [ios_extension](#ios_extension) +* [ios_framework](#ios_framework) (_experimental_) +* [tvos_application](#tvos_application) +* [tvos_extension](#tvos_extension) +* [watchos_application](#watchos_application) +* [watchos_extension](#watchos_extension) + +## Other types + +* [apple_product_type](#apple_product_type) + +## Setup + +Add the following to your `WORKSPACE` file to add the external repositories: + +```python +git_repository( + name = "build_bazel_rules_apple", + remote = "https://github.com/bazelbuild/rules_apple.git", + tag = "0.1.0", +) +``` + +## Examples + +Minimal example: + +```python +load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application") + +objc_library( + name = "Lib", + srcs = glob([ + "**/*.h", + "**/*.m", + ]), + resources = [ + ":Main.storyboard", + ], +) + +# Links code from "deps" into an executable, collects and compiles resources +# from "deps" and places them with the executable in an .app bundle, and then +# outputs an .ipa with the bundle in its Payload directory. +ios_application( + name = "App", + bundle_id = "com.example.app", + families = ["iphone", "ipad"], + infoplists = [":Info.plist"], + deps = [":Lib"], +) +``` + +See the [examples](https://github.com/bazelbuild/rules_apple/tree/master/examples) +directory for sample applications. + +## Migrating from the built-in rules + +Even though the rules in this repository have the same names as their built-in +counterparts, they cannot be intermixed; for example, an `ios_application` from +this repository cannot have an extension that is a built-in `ios_extension` or +vice versa. + +More comprehensive documentation about migrating from the built-in Bazel rules +will be provided soon. + +## Coming soon + +* macOS support +* Support for compiling texture atlases +* Improved rules for creating resource bundles + +## ios_application + +```python +ios_application(name, app_icons, bundle_id, entitlements, extensions, families, +frameworks, infoplists, ipa_post_processor, launch_images, launch_storyboard, +linkopts, product_type, provisioning_profile, settings_bundle, strings, deps) +``` + +Builds and bundles an iOS application. + +The named target produced by this macro is an IPA file. This macro also creates +a target named `{name}.apple_binary` that represents the linked executable +inside the application bundle. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
name +

Name, required

+

A unique name for the target.

+
app_icons +

List of labels; optional

+

Files that comprise the app icons for the application. Each file + must have a containing directory named *.xcassets/*.appiconset and + there may be only one such .appiconset directory in the list.

+
bundle_id +

String; required

+

The bundle ID (reverse-DNS path followed by app name) of the + application.

+
entitlements +

Label; optional

+

The entitlements file required for device builds of the application. + If absent, the default entitlements from the provisioning profile will + be used.

+

The following variables are substituted in the entitlements file: + $(CFBundleIdentifier) with the bundle ID of the application + and $(AppIdentifierPrefix) with the value of the + ApplicationIdentifierPrefix key from the target's + provisioning profile.

+
extensions +

List of labels; optional

+

A list of extensions (see ios_extension) + to include in the final application bundle.

+
families +

List of strings; required

+

A list of device families supported by this application. Valid values + are iphone and ipad; at least one must be specified.

+
frameworks +

List of labels; optional

+

A list of framework targets (see ios_framework) + that this application depends on.

+
infoplists +

List of labels; required

+

A list of .plist files that will be merged to form the + Info.plist that represents the application. At least one + file must be specified.

+
ipa_post_processor +

Label; optional

+

A tool that edits this target's IPA output after it is assembled but + before it is signed. The tool is invoked with a single command-line + argument that denotes the path to a directory containing the unzipped + contents of the IPA (that is, the Payload directory will + be present in this directory).

+

Any changes made by the tool must be made in this directory, and + the tool's execution must be hermetic given these inputs to ensure that + the result can be safely cached.

+
launch_images +

List of labels; optional

+

Files that comprise the launch images for the application. Each file + must have a containing directory named*.xcassets/*.launchimage and + there may be only one such .launchimage directory in the list.

+

It is recommended that you use a launch_storyboard instead if + you are targeting only iOS 8 and later.

+
launch_storyboard +

Label; optional

+

The .storyboard or .xib file that should + be used as the launch screen for the application. The provided file will + be compiled into the appropriate format (.storyboardc or + .nib) and placed in the root of the final bundle. The + generated file will also be registered in the bundle's Info.plist + under the key UILaunchStoryboardName.

+
linkopts +

List of strings; optional

+

A list of strings representing extra flags that the underlying + apple_binary target created by this rule should pass to the + linker.

+
product_type +

String; optional

+

An optional string denoting a special type of application, such as + a Messages Application in iOS 10 and higher. See + apple_product_type.

+
provisioning_profile +

Label; optional

+

The provisioning profile (.mobileprovision file) to use + when bundling the application. This value is optional (and unused) for + simulator builds but required for device builds.

+
settings_bundle +

List of labels; optional

+

An objc_bundle target that contains the files that make up + the application's settings bundle. These files will be copied into the + root of the final application bundle in a directory named + Settings.bundle.

+
strings +

List of labels; optional

+

A list of .strings files, often localizable. These files + are converted to binary plists (if they are not already) and placed in the + root of the final application bundle, unless a file's immediate containing + directory is named *.lproj, in which case it will be placed + under a directory with the same name in the bundle.

+
watch_application +

Label; optional

+

A watchos_application target that represents an Apple + Watch application that should be embedded in the application.

+
deps +

List of labels; optional

+

A list of dependencies targets that are passed into the + apple_binary rule to be linked. Any resources, such as + asset catalogs, that are referenced by those targets will also be + transitively included in the final application.

+
+ +## ios_extension + +```python +ios_extension(name, app_icons, bundle_id, entitlements, families, frameworks, +infoplists, ipa_post_processor, linkopts, product_type, provisioning_profile, +strings, deps) +``` + +Builds and bundles an iOS application extension. + +The named target produced by this macro is a ZIP file. This macro also creates a +target named `{name}.apple_binary` that represents the linked binary +executable inside the extension bundle. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
name +

Name, required

+

A unique name for the target.

+
app_icons +

List of labels; optional

+

Files that comprise the app icons for the extension. Each file + must have a containing directory named*.xcassets/*.appiconset and + there may be only one such .appiconset directory in the list.

+
bundle_id +

String; required

+

The bundle ID (reverse-DNS path followed by app name) of the + extension.

+
entitlements +

Label; optional

+

The entitlements file required for device builds of the extension. + If absent, the default entitlements from the provisioning profile will + be used.

+

The following variables are substituted in the entitlements file: + $(CFBundleIdentifier) with the bundle ID of the extension + and $(AppIdentifierPrefix) with the value of the + ApplicationIdentifierPrefix key from the target's + provisioning profile.

+
families +

List of strings; required

+

A list of device families supported by this extension. Valid values + are iphone and ipad; at least one must be specified.

+
frameworks +

List of labels; optional

+

A list of framework targets (see ios_framework) + that this extension depends on.

+
infoplists +

List of labels; required

+

A list of .plist files that will be merged to form the + Info.plist that represents the extension. At least one + file must be specified.

+
ipa_post_processor +

Label; optional

+

A tool that edits this target's archive after it is assembled but + before it is signed. The tool is invoked with a single command-line + argument that denotes the path to a directory containing the unzipped + contents of the archive; the *.appex bundle for the + extension will be the directory's only contents.

+

Any changes made by the tool must be made in this directory, and + the tool's execution must be hermetic given these inputs to ensure that + the result can be safely cached.

+
linkopts +

List of strings; optional

+

A list of strings representing extra flags that the underlying + apple_binary target created by this rule should pass to the + linker.

+
product_type +

String; optional

+

An optional string denoting a special type of extension, such as + a Messages Extension in iOS 10 and higher. See + apple_product_type.

+
provisioning_profile +

Label; optional

+

The provisioning profile (.mobileprovision file) to use + when bundling the extension. This value is optional (and unused) for + simulator builds but required for device builds.

+
strings +

List of labels; optional

+

A list of .strings files, often localizable. These files + are converted to binary plists (if they are not already) and placed in the + root of the final extension bundle, unless a file's immediate containing + directory is named *.lproj, in which case it will be placed + under a directory with the same name in the bundle.

+
deps +

List of labels; optional

+

A list of dependencies targets that are passed into the + apple_binary rule to be linked. Any resources, such as + asset catalogs, that are referenced by those targets will also be + transitively included in the final extension.

+
+ +## ios_framework + +```python +ios_framework(name, bundle_id, families, infoplists, ipa_post_processor, +linkopts, strings, deps) +``` + +Builds and bundles an iOS dynamic framework. + +The named target produced by this macro is a ZIP file. This macro also creates a +target named `{name}.apple_binary` that represents the linked dynamic library +inside the framework bundle. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
name +

Name, required

+

A unique name for the target.

+
bundle_id +

String; required

+

The bundle ID (reverse-DNS path followed by app name) of the + framework.

+
families +

List of strings; required

+

A list of device families supported by this framework. Valid values + are iphone and ipad; at least one must be specified.

+
infoplists +

List of labels; required

+

A list of .plist files that will be merged to form the + Info.plist that represents the framework. At least one + file must be specified.

+
ipa_post_processor +

Label; optional

+

A tool that edits this target's archive after it is assembled but + before it is signed. The tool is invoked with a single command-line + argument that denotes the path to a directory containing the unzipped + contents of the archive; the *.framework bundle for the + extension will be the directory's only contents.

+

Any changes made by the tool must be made in this directory, and + the tool's execution must be hermetic given these inputs to ensure that + the result can be safely cached.

+
linkopts +

List of strings; optional

+

A list of strings representing extra flags that the underlying + apple_binary target created by this rule should pass to the + linker.

+
strings +

List of labels; optional

+

A list of .strings files, often localizable. These files + are converted to binary plists (if they are not already) and placed in the + root of the final extension bundle, unless a file's immediate containing + directory is named *.lproj, in which case it will be placed + under a directory with the same name in the bundle.

+
deps +

List of labels; optional

+

A list of dependencies targets that are passed into the + apple_binary rule to be linked. Any resources, such as + asset catalogs, that are referenced by those targets will also be + transitively included in the final framework.

+
+ +## tvos_application + +```python +tvos_application(name, app_icons, bundle_id, entitlements, extensions, +infoplists, ipa_post_processor, launch_images, launch_storyboard, linkopts, +provisioning_profile, settings_bundle, strings, deps) +``` + +Builds and bundles a tvOS application. + +The named target produced by this macro is an IPA file. This macro also creates +a target named `{name}.apple_binary` that represents the linked executable +inside the application bundle. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
name +

Name, required

+

A unique name for the target.

+
app_icons +

List of labels; optional

+

Files that comprise the app icons for the application. Each file + must have a containing directory named*.xcassets/*.appiconset and + there may be only one such .appiconset directory in the list.

+
bundle_id +

String; required

+

The bundle ID (reverse-DNS path followed by app name) of the + application.

+
entitlements +

Label; optional

+

The entitlements file required for device builds of the application. + If absent, the default entitlements from the provisioning profile will + be used.

+

The following variables are substituted in the entitlements file: + $(CFBundleIdentifier) with the bundle ID of the application + and $(AppIdentifierPrefix) with the value of the + ApplicationIdentifierPrefix key from the target's + provisioning profile.

+
extensions +

List of labels; optional

+

A list of extensions (see tvos_extension) + to include in the final application bundle.

+
infoplists +

List of labels; required

+

A list of .plist files that will be merged to form the + Info.plist that represents the application. At least one + file must be specified.

+
ipa_post_processor +

Label; optional

+

A tool that edits this target's IPA output after it is assembled but + before it is signed. The tool is invoked with a single command-line + argument that denotes the path to a directory containing the unzipped + contents of the IPA (that is, the Payload directory will + be present in this directory).

+

Any changes made by the tool must be made in this directory, and + the tool's execution must be hermetic given these inputs to ensure that + the result can be safely cached.

+
launch_images +

List of labels; optional

+

Files that comprise the launch images for the application. Each file + must have a containing directory named*.xcassets/*.launchimage and + there may be only one such .launchimage directory in the list.

+

It is recommended that you use a launch_storyboard instead if + you are targeting only iOS 8 and later.

+
launch_storyboard +

Label; optional

+

The .storyboard or .xib file that should + be used as the launch screen for the application. The provided file will + be compiled into the appropriate format (.storyboardc or + .nib) and placed in the root of the final bundle. The + generated file will also be registered in the bundle's Info.plist + under the key UILaunchStoryboardName.

+
linkopts +

List of strings; optional

+

A list of strings representing extra flags that the underlying + apple_binary target created by this rule should pass to the + linker.

+
provisioning_profile +

Label; optional

+

The provisioning profile (.mobileprovision file) to use + when bundling the application. This value is optional (and unused) for + simulator builds but required for device builds.

+
settings_bundle +

List of labels; optional

+

An objc_bundle target that contains the files that make up + the application's settings bundle. These files will be copied into the + root of the final application bundle in a directory named + Settings.bundle.

+
strings +

List of labels; optional

+

A list of .strings files, often localizable. These files + are converted to binary plists (if they are not already) and placed in the + root of the final application bundle, unless a file's immediate containing + directory is named *.lproj, in which case it will be placed + under a directory with the same name in the bundle.

+
deps +

List of labels; optional

+

A list of dependencies targets that are passed into the + apple_binary rule to be linked. Any resources, such as + asset catalogs, that are referenced by those targets will also be + transitively included in the final application.

+
+ +## tvos_extension + +```python +tvos_extension(name, bundle_id, entitlements, infoplists, ipa_post_processor, +linkopts, strings, deps) +``` + +Builds and bundles a tvOS extension. + +The named target produced by this macro is a ZIP file. This macro also creates a +target named `{name}.apple_binary` that represents the linked binary +executable inside the extension bundle. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
name +

Name, required

+

A unique name for the target.

+
bundle_id +

String; required

+

The bundle ID (reverse-DNS path followed by app name) of the + extension.

+
entitlements +

Label; optional

+

The entitlements file required for device builds of the extension. + If absent, the default entitlements from the provisioning profile will + be used.

+

The following variables are substituted in the entitlements file: + $(CFBundleIdentifier) with the bundle ID of the extension + and $(AppIdentifierPrefix) with the value of the + ApplicationIdentifierPrefix key from the target's + provisioning profile.

+
infoplists +

List of labels; required

+

A list of .plist files that will be merged to form the + Info.plist that represents the extension. At least one + file must be specified.

+
ipa_post_processor +

Label; optional

+

A tool that edits this target's archive after it is assembled but + before it is signed. The tool is invoked with a single command-line + argument that denotes the path to a directory containing the unzipped + contents of the archive; the *.appex bundle for the + extension will be the directory's only contents.

+

Any changes made by the tool must be made in this directory, and + the tool's execution must be hermetic given these inputs to ensure that + the result can be safely cached.

+
linkopts +

List of strings; optional

+

A list of strings representing extra flags that the underlying + apple_binary target created by this rule should pass to the + linker.

+
provisioning_profile +

Label; optional

+

The provisioning profile (.mobileprovision file) to use + when bundling the extension. This value is optional (and unused) for + simulator builds but required for device builds.

+
strings +

List of labels; optional

+

A list of .strings files, often localizable. These files + are converted to binary plists (if they are not already) and placed in the + root of the final extension bundle, unless a file's immediate containing + directory is named *.lproj, in which case it will be placed + under a directory with the same name in the bundle.

+
deps +

List of labels; optional

+

A list of dependencies targets that are passed into the + apple_binary rule to be linked. Any resources, such as + asset catalogs, that are referenced by those targets will also be + transitively included in the final extension.

+
+ +## watchos_application + +```python +watchos_application(name, app_icons, bundle_id, entitlements, extension, +infoplists, ipa_post_processor, provisioning_profile, storyboards, strings, +deps) +``` + +Builds and bundles a watchOS application. + +**This rule only supports watchOS 2.0 and higher.** Apple no longer supports +or accepts submissions of apps written for watchOS 1.x, so these bundling rules +do not support that version of the platform. + +The named target produced by this macro is a ZIP file. The watch application is +not executable or installable by itself; the target must be added to a +companion `ios_application` using the `watch_application` attribute on that +rule. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
name +

Name, required

+

A unique name for the target.

+
app_icons +

List of labels; optional

+

Files that comprise the app icons for the application. Each file + must have a containing directory named*.xcassets/*.appiconset and + there may be only one such .appiconset directory in the list.

+
bundle_id +

String; required

+

The bundle ID (reverse-DNS path followed by app name) of the + application.

+
entitlements +

Label; optional

+

The entitlements file required for device builds of the application. + If absent, the default entitlements from the provisioning profile will + be used.

+

The following variables are substituted in the entitlements file: + $(CFBundleIdentifier) with the bundle ID of the application + and $(AppIdentifierPrefix) with the value of the + ApplicationIdentifierPrefix key from the target's + provisioning profile.

+
extension +

Label; required

+

The watchos_extension + that is bundled with the watch application.

+
infoplists +

List of labels; required

+

A list of .plist files that will be merged to form the + Info.plist that represents the application. At least one + file must be specified.

+
ipa_post_processor +

Label; optional

+

A tool that edits this target's IPA output after it is assembled but + before it is signed. The tool is invoked with a single command-line + argument that denotes the path to a directory containing the unzipped + contents of the IPA (that is, the Payload directory will + be present in this directory).

+

Any changes made by the tool must be made in this directory, and + the tool's execution must be hermetic given these inputs to ensure that + the result can be safely cached.

+
provisioning_profile +

Label; optional

+

The provisioning profile (.mobileprovision file) to use + when bundling the application. This value is optional (and unused) for + simulator builds but required for device builds.

+
storyboards +

List of labels; optional

+

A list of .storyboard files, often localizable. These files + are compiled and placed in the root of the final application bundle, unless + a file's immediate containing directory is named *.lproj, in + which case it will be placed under a directory with the same name in the + bundle.

+
strings +

List of labels; optional

+

A list of .strings files, often localizable. These files + are converted to binary plists (if they are not already) and placed in the + root of the final application bundle, unless a file's immediate containing + directory is named *.lproj, in which case it will be placed + under a directory with the same name in the bundle.

+
deps +

List of labels; optional

+

A list of targets whose resources will be included in the final + application. Since a watchOS application does not contain any code of + its own, any code in the dependent libraries will be ignored.

+
+ +## watchos_extension + +```python +watchos_extension(name, app_icons, bundle_id, entitlements, infoplists, +ipa_post_processor, linkopts, provisioning_profile, strings, deps) +``` + +Builds and bundles a watchOS extension. + +**This rule only supports watchOS 2.0 and higher.** Apple no longer supports +or accepts submissions of apps written for watchOS 1.x, so these bundling rules +do not support that version of the platform. + +The named target produced by this macro is a ZIP file. This macro also creates a +target named `{name}.apple_binary` that represents the linked binary +executable inside the extension bundle. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributes
name +

Name, required

+

A unique name for the target.

+
app_icons +

List of labels; optional

+

Files that comprise the app icons for the extension. Each file + must have a containing directory named*.xcassets/*.appiconset and + there may be only one such .appiconset directory in the list.

+
bundle_id +

String; required

+

The bundle ID (reverse-DNS path followed by app name) of the + extension.

+
entitlements +

Label; optional

+

The entitlements file required for device builds of the extension. + If absent, the default entitlements from the provisioning profile will + be used.

+

The following variables are substituted in the entitlements file: + $(CFBundleIdentifier) with the bundle ID of the extension + and $(AppIdentifierPrefix) with the value of the + ApplicationIdentifierPrefix key from the target's + provisioning profile.

+
infoplists +

List of labels; required

+

A list of .plist files that will be merged to form the + Info.plist that represents the extension. At least one + file must be specified.

+
ipa_post_processor +

Label; optional

+

A tool that edits this target's archive after it is assembled but + before it is signed. The tool is invoked with a single command-line + argument that denotes the path to a directory containing the unzipped + contents of the archive; the *.appex bundle for the + extension will be the directory's only contents.

+

Any changes made by the tool must be made in this directory, and + the tool's execution must be hermetic given these inputs to ensure that + the result can be safely cached.

+
linkopts +

List of strings; optional

+

A list of strings representing extra flags that the underlying + apple_binary target created by this rule should pass to the + linker.

+
provisioning_profile +

Label; optional

+

The provisioning profile (.mobileprovision file) to use + when bundling the extension. This value is optional (and unused) for + simulator builds but required for device builds.

+
strings +

List of labels; optional

+

A list of .strings files, often localizable. These files + are converted to binary plists (if they are not already) and placed in the + root of the final extension bundle, unless a file's immediate containing + directory is named *.lproj, in which case it will be placed + under a directory with the same name in the bundle.

+
deps +

List of labels; optional

+

A list of dependencies targets that are passed into the + apple_binary rule to be linked. Any resources, such as + asset catalogs, that are referenced by those targets will also be + transitively included in the final extension.

+
+ +## apple_product_type + +A `struct` containing product type identifiers used by special application and +extension types. + +Some applications and extensions, such as Messages Extensions and +Sticker Packs in iOS 10, receive special treatment when building (for example, +some product types bundle a stub executable instead of a user-defined binary, +and some pass extra arguments to tools like the asset compiler). These +behaviors are captured in the product type identifier. The product types +currently supported are: + + + + + + + + + + + + + + + + + + + + + + + + + +
Product types
messages_application +

Applies to ios_application targets built for iOS 10 and + above.

+

A "stub" application used to distribute a standalone Messages + Extension or Sticker Pack. This application must + include an ios_extension whose product type is + messages_extension or + messages_sticker_pack_extension (or it can include both). +

+

This product type does not contain a user-provided binary; any code + in its deps will be ignored.

+

This stub application is not displayed on the home screen and its + features are only accessible through the Messages user interface. If + you are building a Messages Extension or Sticker Pack as part of a + larger application that is launchable, do not use this product type; + simply add those extensions to the existing application.

+
messages_extension +

Applies to ios_extension targets built for iOS 10 and + above.

+

An extension that integrates custom behavior into the Apple Messages + application. Such extensions can present a custom user interface in the + keyboard area of the app and interact with users' conversations.

+
messages_sticker_pack_extension +

Applies to ios_extension targets built for iOS 10 and + above.

+

An extension that defines custom sticker packs for the Apple + Messages app. Stickers are provided by including an asset catalog + named *.xcstickers in the extension's + asset_catalogs attribute.

+

This product type does not contain a user-provided binary; any + code in its deps will be ignored.

+
+ +Example usage: + +```python +load("@build_bazel_rules_apple//apple:ios.bzl", + "apple_product_type", "ios_application", "ios_extension") + +ios_application( + name = "StickerPackApp", + extensions = [":StickerPackExtension"], + product_type = apple_product_type.messages_application, + # other attributes... +) + +ios_extension( + name = "StickerPackExtension", + product_type = apple_product_type.messages_sticker_pack_extension, + # other attributes... +) +``` diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000000..2cbc206bb2 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "build_bazel_rules_apple") diff --git a/apple/BUILD b/apple/BUILD new file mode 100644 index 0000000000..e9aa2f7b2b --- /dev/null +++ b/apple/BUILD @@ -0,0 +1,31 @@ +filegroup( + name = "rules", + srcs = glob(["*.bzl"]), + visibility = ["//visibility:public"], +) + +# Consumed by bazel tests. +filegroup( + name = "for_bazel_tests", + testonly = 1, + srcs = glob(["**"]) + [ + "//apple/bundling:for_bazel_tests", + ], + visibility = [ + "//test:__subpackages__", + ], +) + +[ + config_setting( + name = "ios_cpu_" + arch, + values = {"ios_cpu": arch}, + visibility = ["//visibility:public"], + ) + for arch in [ + "i386", + "x86_64", + "armv7", + "arm64", + ] +] diff --git a/apple/bundling/BUILD b/apple/bundling/BUILD new file mode 100644 index 0000000000..4a8b6310b9 --- /dev/null +++ b/apple/bundling/BUILD @@ -0,0 +1,40 @@ +filegroup( + name = "bundler_py", + srcs = ["bundler.py"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "bundling", + srcs = glob(["*.bzl"]), + visibility = ["//apple:__subpackages__"], +) + +filegroup( + name = "dsym_info_plist_template", + srcs = ["dSYM-Info.plist.template"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "plisttool", + srcs = ["plisttool.py"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "process_and_sign_template", + srcs = ["process_and_sign.sh.template"], + visibility = ["//visibility:public"], +) + +# Consumed by bazel tests. +filegroup( + name = "for_bazel_tests", + testonly = 1, + srcs = glob(["**"]), + visibility = [ + "//apple:__subpackages__", + "//test:__subpackages__", + ], +) diff --git a/apple/bundling/README.md b/apple/bundling/README.md new file mode 100644 index 0000000000..61779ded06 --- /dev/null +++ b/apple/bundling/README.md @@ -0,0 +1,16 @@ +# Internal Apple bundling logic + +The `.bzl` files in this directory are internal implementation details for the +Apple bundling rules. They should not be imported directly by users; instead, +import the platform-specific rules in `//apple:ios.bzl`, `//apple:tvos.bzl`, +and so forth. + +As a matter of style, files ending in `_actions.bzl` export modules with +functions that register specific actions (such as compiling Interface Builder +files with `ibtool`), whereas files ending in `_support.bzl` export modules +with general support functions (most of which do not register actions, but some +of which do in a generic sense, such as `xcode_env_action` in +`platform_support`). This separation helps to avoid circular dependencies in +Skylark `load` statements (because actions are only registered in one place, +but support functions may be needed in multiple places throughout the bundling +codebase). diff --git a/apple/bundling/apple_bundling_aspect.bzl b/apple/bundling/apple_bundling_aspect.bzl new file mode 100644 index 0000000000..6c81354692 --- /dev/null +++ b/apple/bundling/apple_bundling_aspect.bzl @@ -0,0 +1,287 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An aspect that collects information used during Apple bundling.""" + +load("//apple:providers.bzl", + "AppleBundlingSwift", + "AppleResource", + "AppleResourceSet", + "apple_resource_set_utils", + ) +load("//apple:utils.bzl", + "basename", + "group_files_by_directory" + ) +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:provider_support.bzl", + "provider_support") + + +def _attr_files(ctx, name): + """Returns the list of files for the current target's attribute. + + This is a convenience function since the aspect context does not expose the + same convenience `file`/`files` fields used in rule contexts. + + Args: + ctx: The Skylark context. + name: The name of the attribute. + Returns: + A list of Files. + """ + return [f for t in getattr(ctx.rule.attr, name) for f in t.files] + + +def _handle_native_library_dependency(target, ctx): + """Handles resources from an `objc_library` or `objc_bundle_library`. + + Args: + target: The target to which the aspect is being applied. + ctx: The Skylark context. + Returns: + A list of `AppleResourceSet` values that should be included in the list + propagated by the `AppleResource` provider. + """ + resource_sets = [] + bundles = getattr(ctx.rule.attr, "bundles", []) + + if ctx.rule.kind == "objc_bundle_library": + bundle_dir = target.label.name + ".bundle" + infoplists = [] + if ctx.rule.attr.infoplist: + infoplists.append(list(ctx.rule.attr.infoplist.files)[0]) + infoplists.extend(_attr_files(ctx, "infoplists")) + + # The "bundles" attribute of an objc_bundle_library indicates bundles that + # should be nested inside that target's .bundle directory. Since we pass + # transitive resources as a flat list based on their .bundle directory, we + # must prepend the current target's {name}.bundle to the path so that the + # files end up in the correct place. + for p in provider_support.matching_providers(bundles, "AppleResource"): + resource_sets.extend([ + apple_resource_set_utils.prefix_bundle_dir(rs, bundle_dir) + for rs in p.resource_sets + ]) + elif ctx.rule.kind == "objc_library": + bundle_dir = None + infoplists = [] + + # The "bundles" attribute of an objc_library don't indicate a nesting + # relationship, so simply bring them over as-is. + for p in provider_support.matching_providers(bundles, "AppleResource"): + resource_sets.extend(p.resource_sets) + else: + fail(("Internal consistency error: expected rule to be objc_library " + + "objc_bundle_library, but got %s") % ctx.rule.kind) + + # Then, build the bundled_resources struct for the resources directly in the + # current target. + resources = depset(ctx.rule.files.asset_catalogs + + ctx.rule.files.datamodels + + ctx.rule.files.resources + + ctx.rule.files.storyboards + + ctx.rule.files.strings + + ctx.rule.files.xibs) + structured_resources = depset(ctx.rule.files.structured_resources) + + # Only create the resource set if it's non-empty. + if resources or infoplists or structured_resources: + resource_sets.append(AppleResourceSet( + bundle_dir=bundle_dir, + infoplists=depset(infoplists), + resources=resources, + structured_resources=structured_resources, + )) + + return resource_sets + + +def _handle_native_bundle_imports(bundle_imports): + """Handles resources from an `objc_bundle` target. + + Args: + bundle_imports: The list of `File`s in the bundle. + Returns: + A list of `AppleResourceSet` values that should be included in the list + propagated by the `AppleResource` provider. + """ + grouped_bundle_imports = group_files_by_directory( + bundle_imports, ["bundle"], "bundle_imports") + + resource_sets = [] + + # objc_bundles are copied verbatim into the bundle, preserving the directory + # structure, but without any extra path prefixes before the ".bundle" + # segment. We pass these along as a special case for the bundler to handle. + for bundle_dir, files in grouped_bundle_imports.items(): + # We use basename in case the path to the bundle includes other segments + # (like foo/bar/baz.bundle), which is allowed. + resource_sets.append(AppleResourceSet( + bundle_dir = basename(bundle_dir), + objc_bundle_imports = depset(files), + )) + + return resource_sets + + +def _handle_unknown_objc_provider(objc): + """Handles resources from a target that propagates an `objc` provider. + + This method is called as a last resort for targets not already handled + elsewhere (like `objc_library`), since some users are currently propagating + resources by creating their own `objc` provider. + + Args: + objc: The `objc` provider. + Returns: + An `AppleResourceSet` value that should be included in the list propagated + by the `AppleResource` provider. + """ + resources = (objc.asset_catalog + + objc.storyboard + + objc.strings + + objc.xcdatamodel + + objc.xib) + + # Only create the resource set if it's non-empty. + if not (resources or objc.bundle_file or objc.merge_zip): + return None + + # Assume that any bundlable files whose bundle paths are just their basenames + # had their paths flattened (if they were nested to begin with) and they can + # be treated as resources. + resources += [bf.file for bf in objc.bundle_file + if bf.bundle_path == bf.file.basename] + + # Bundlable files whose bundle paths are not just their basenames should be + # treated as structured resources to preserve those paths. + structured_resources = depset([bf.file for bf in objc.bundle_file + if bf.bundle_path != bf.file.basename]) + + return AppleResourceSet( + resources=resources, + structured_resources=structured_resources, + structured_resource_zips=objc.merge_zip, + ) + + +def _transitive_apple_resource(target, ctx): + """Builds the `AppleResource` provider to be propagated. + + Args: + target: The target to which the aspect is being applied. + ctx: The Skylark context. + Returns: + An `AppleResource` provider, or `None` if nothing should be propagated for + this target. + """ + resource_sets = [] + + if hasattr(target, "AppleResource"): + resource_sets.extend(target.AppleResource.resource_sets) + elif ctx.rule.kind in ("objc_library", "objc_bundle_library"): + resource_sets.extend(_handle_native_library_dependency(target, ctx)) + elif ctx.rule.kind == "objc_bundle": + bundle_imports = ctx.rule.files.bundle_imports + resource_sets.extend(_handle_native_bundle_imports(bundle_imports)) + + # If the rule has deps, propagate the transitive info from this target's + # dependencies. + deps = getattr(ctx.rule.attr, "deps", []) + for p in provider_support.matching_providers(deps, "AppleResource"): + resource_sets.extend(p.resource_sets) + + # Handle arbitrary objc providers, but only if we haven't gotten resource + # sets for the target or its deps already. This lets us handle "resource + # leaf nodes" (custom rules that return resources via the objc provider) + # until they migrate to AppleResource, but without pulling in duplicated + # information from the transitive objc providers on the way back up + # (because we'll have already gotten that information in the form we want + # from the transitive AppleResource providers). + if not resource_sets and hasattr(target, "objc"): + resource_set = _handle_unknown_objc_provider(target.objc) + if resource_set: + resource_sets.append(resource_set) + + if resource_sets: + minimized = apple_resource_set_utils.minimize(resource_sets) + return AppleResource(resource_sets=minimized) + else: + return None + + +def _transitive_apple_bundling_swift(target, ctx): + """Builds the `AppleBundlingSwift` provider to be propagated. + + Args: + target: The target to which the aspect is being applied. + ctx: The Skylark context. + Returns: + An `AppleBundlingSwift` provider, or `None` if nothing should be propagated + for this target. + """ + uses_swift = hasattr(target, "swift") + + # If the target itself doesn't use Swift, check its deps. + if not uses_swift: + deps = getattr(ctx.rule.attr, "deps", []) + providers = provider_support.matching_providers(deps, "AppleBundlingSwift") + uses_swift = any([p.uses_swift for p in providers]) + + return AppleBundlingSwift(uses_swift=uses_swift) + + +def _apple_bundling_aspect_impl(target, ctx): + """Implementation of `apple_bundling_aspect`. + + This implementation fans out the handling of each of its providers to a + separate function. + + Args: + target: The target on which the aspect is being applied. + ctx: The Skylark context. + Returns: + A struct with providers for the aspect. Refer to the rule documentation for + a description of these providers. + """ + apple_resource = _transitive_apple_resource(target, ctx) + apple_bundling_swift = _transitive_apple_bundling_swift(target, ctx) + + providers = {} + if apple_resource: + providers["AppleResource"] = apple_resource + if apple_bundling_swift: + providers["AppleBundlingSwift"] = apple_bundling_swift + + return struct(**providers) + + +apple_bundling_aspect = aspect( + implementation = _apple_bundling_aspect_impl, + attr_aspects = ["bundles", "deps"], +) +""" +This aspect walks the dependency graph through the `deps` attribute and +collects information needed during the bundling process. For example, we +determine whether Swift is used anywhere within the dependency chain, and for +resources that need to be associated with a module when compiled (data models, +storyboards, and XIBs), we annotate those resources with that information as +well. + +This aspect may propagate the `AppleResource` and `AppleBundlingSwift` +providers. Refer to the documentation for those providers for a description of +the fields they contain. +""" diff --git a/apple/bundling/binary_support.bzl b/apple/bundling/binary_support.bzl new file mode 100644 index 0000000000..43a9d536c6 --- /dev/null +++ b/apple/bundling/binary_support.bzl @@ -0,0 +1,131 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Binary creation support functions.""" + +load("//apple/bundling:entitlements.bzl", + "entitlements", + "entitlements_support") +load("//apple/bundling:product_support.bzl", + "product_support") + + +def _create_binary_if_necessary( + name, + platform_type, + sdk_frameworks=[], + **kwargs): + """Creates a binary target if necessary for a bundle. + + This function also wraps the entitlements handling logic. It returns a + modified copy of the given keyword arguments that has `binary` and + `entitlements` attributes added if necessary and removes other + binary-specific options (such as `linkopts`). + + Some Apple bundles may not need a binary target depending on their product + type; for example, watchOS applications and iMessage sticker packs contain + stub binaries copied from the platform SDK, rather than binaries with user + code. + + This function must be called from one of the top-level application or + extension macros, because it invokes a rule to create a target. As such, it + cannot be called within rule implementation functions. + + Args: + name: The name of the bundle target, from which the binary target's name + will be derived. + platform_type: The platform type for which the binary should be built. + sdk_frameworks: Additional SDK frameworks that should be linked with the + final binary. + **kwargs: The arguments that were passed into the top-level macro. + Returns: + A modified copy of `**kwargs` that should be passed to the bundling rule. + """ + bundling_args = dict(kwargs) + + # If a user provides a "binary" attribute of their own, it is ignored and + # silently overwritten below. Instead of allowing this, we should fail fast + # to prevent confusion. + if "binary" in bundling_args: + fail("Do not provide your own binary; one will be linked from your deps.", + attr="binary") + + entitlements_value = bundling_args.pop("entitlements", None) + linkopts = bundling_args.pop("linkopts", []) + provisioning_profile = kwargs.get("provisioning_profile") + + if provisioning_profile: + # Generate the debug and device entitlements regardless of whether we're + # creating a binary or using a stub. + entitlements_name = "%s_entitlements" % name + entitlements( + name = entitlements_name, + bundle_id = kwargs.get("bundle_id"), + entitlements = entitlements_value, + platform_type = platform_type, + provisioning_profile = provisioning_profile, + ) + bundling_args["entitlements"] = entitlements_support.device_file_label( + entitlements_name) + entitlements_srcs = [ + entitlements_support.simulator_file_label(entitlements_name) + ] + entitlements_deps = [":" + entitlements_name] + else: + entitlements_srcs = [] + entitlements_deps = [] + + # Now, figure out if the product type uses a stub binary. If not, create the + # target for the user's binary. + product_info = None + product_type = (kwargs.get("product_type") or + bundling_args.pop("_product_type", None)) + if product_type: + product_info = product_support.product_type_info(product_type) + + if not product_info: + deps = kwargs.get("deps", []) + dylibs = kwargs.get("frameworks") + if not deps: + fail("This target must provide deps because it is of a product type " + + "that requires a user binary.") + + # Link the executable from any library deps and sources provided. + apple_binary_name = "%s.apple_binary" % name + linkopts += ["-rpath", "@executable_path/../../Frameworks"] + + # Pass the entitlements target as an extra dependency to the binary rule + # to pick up the extra linkopts (if any) propagated by it. + native.apple_binary( + name = apple_binary_name, + srcs = entitlements_srcs, + features = kwargs.get("features"), + linkopts = linkopts, + platform_type = platform_type, + sdk_frameworks = sdk_frameworks, + deps = deps + entitlements_deps, + dylibs = dylibs, + tags = kwargs.get("tags"), + testonly = kwargs.get("testonly"), + visibility = kwargs.get("visibility"), + ) + bundling_args["binary"] = apple_binary_name + + return bundling_args + + +# Define the loadable module that lists the exported symbols in this file. +binary_support = struct( + create_binary_if_necessary=_create_binary_if_necessary, +) diff --git a/apple/bundling/bundler.bzl b/apple/bundling/bundler.bzl new file mode 100644 index 0000000000..7efbc0dfb8 --- /dev/null +++ b/apple/bundling/bundler.bzl @@ -0,0 +1,851 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core bundling support used by the Apple rules. + +This file is only meant to be imported by the platform-specific top-level rules +(ios.bzl, tvos.bzl, and so forth). +""" + +load("//apple:providers.bzl", + "AppleResource", + "AppleResourceSet", + "apple_resource_set_utils", + ) +load("//apple:utils.bzl", + "basename", + "bash_array_string", + "bash_quote", + "dirname", + "group_files_by_directory", + "optionally_prefixed_path", + "relativize_path", + "remove_extension", + ) +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:codesigning_support.bzl", + "codesigning_support") +load("//apple/bundling:dsym_actions.bzl", "dsym_actions") +load("//apple/bundling:file_actions.bzl", "file_actions") +load("//apple/bundling:file_support.bzl", "file_support") +load("//apple/bundling:platform_support.bzl", + "platform_support") +load("//apple/bundling:plist_actions.bzl", "plist_actions") +load("//apple/bundling:product_actions.bzl", + "product_actions") +load("//apple/bundling:product_support.bzl", + "product_support") +load("//apple/bundling:provider_support.bzl", + "provider_support") +load("//apple/bundling:resource_actions.bzl", + "resource_actions") +load("//apple/bundling:resource_support.bzl", + "resource_support") +load("//apple/bundling:swift_actions.bzl", "swift_actions") +load("//apple/bundling:swift_support.bzl", "swift_support") + + +# Directories inside .frameworks that should not be included in final +# application/extension bundles. +_FRAMEWORK_DIRS_TO_EXCLUDE = [ + "Headers", "Modules", "PrivateHeaders", +] + + +def _bundlable_files_for_control(bundlable_files): + """Converts a list of bundlable files to be used by bundler.py. + + Bundlable files are stored during the initial analysis with their `src` as + the `File` artifact so that they can be passed as inputs to other actions. + But when writing out the control file for the bundling script, we need to + write out the path names. This function applies that simple conversion. + + Args: + bundlable_files: A list of bundlable file values. + Returns: + A list representing the same bundlable files, but with the `File` objects + replaced by their paths. + """ + return [bundling_support.bundlable_file(bf.src.path, + bf.dest if bf.dest else "", + bf.executable) + for bf in bundlable_files] + + +def _convert_native_bundlable_file(bf, bundle_dir=None): + """Transforms bundlable file values obtained from an `objc` provider. + + The native `objc` provider returns bundlable files as a struct with two keys: + `file` to represent the file being bundled and `bundle_path` for the path + inside the bundle where it should be placed. These rules use a different + format, so this function converts the native format to the one we need. + + This list can also contain Skylark bundlable files (as returned by + `bundling_support.bundlable_file`); they will be returned with the + `bundle_dir` prepended to their destination. + + TODO(b/33618143): Remove this when the native bundlable file type is + removed. + + Args: + bf: A bundlable file, potentially from a native provider. + bundle_dir: If provided, a directory path that will be prepended to the + `bundle_path` of the bundlable file's destination path. + Returns: + A list of bundlable file values corresponding to the inputs, as returned by + `bundling_support.bundlable_file`. + """ + if hasattr(bf, "file") and hasattr(bf, "bundle_path"): + return bundling_support.bundlable_file( + bf.file, optionally_prefixed_path(bf.bundle_path, bundle_dir)) + else: + return bundling_support.bundlable_file( + bf.src, optionally_prefixed_path(bf.dest, bundle_dir), bf.executable) + + +def _bundlable_dynamic_framework_files(ctx, files): + """Computes the set of bundlable files for framework dependencies. + + The `files` argument passed into this function is expected to be a set of + `File`s from the `dynamic_framework_file` key of a dependency's `objc` + provider. This function then returns a set of bundlable files that dictates + where the framework files should go in the final application/extension + bundle, excluding any files that don't need to be packaged with the final + product (such as headers). + + Args: + ctx: The Skylark context. + files: A `depset` of `File`s inside .framework folders that should be merged + into the bundle. + Returns: + A list of bundlable file structs corresponding to the files that should be + copied into the bundle. + """ + bundle_files = [] + + grouped_files = group_files_by_directory(files, ["framework"], "deps") + for framework, framework_files in grouped_files.items(): + framework_name = basename(framework) + for f in framework_files: + relative_path = relativize_path(f.path, framework) + first_segment = relative_path.partition("/")[0] + if first_segment not in _FRAMEWORK_DIRS_TO_EXCLUDE: + bundle_files.append(bundling_support.contents_file( + ctx, f, "Frameworks/%s/%s" % (framework_name, relative_path))) + + return bundle_files + + +def _validate_attributes(ctx): + """Validates the target's attributes and fails the build if any are invalid. + + Args: + ctx: The Skylark context. + """ + families = platform_support.families(ctx) + allowed_families = ctx.attr._allowed_families + for family in families: + if family not in allowed_families: + fail(("One or more of the provided device families \"%s\" is not in the " + + "list of allowed device families \"%s\"") % ( + families, allowed_families)) + + +def _dedupe_bundle_merge_files(bundlable_files): + """Deduplicates bundle files by destination. + + No two resources should be destined for the same location within the + bundle unless they come from the same root-relative source. This removes + duplicates but fails if two different source files are to end up at the same + bundle path. + + Args: + bundlable_files: The list of bundlable files to deduplicate. + Returns: + A list of bundle files with duplicates purged. + """ + deduped_bundlable_files = [] + path_to_files = {} + for bf in bundlable_files: + this_file = bf.src + + other_file = path_to_files.get(bf.dest) + if other_file: + if other_file.short_path != this_file.short_path: + fail(("Multiple files would be placed at \"%s\" in the bundle, " + + "which is not allowed: [%s,%s]") % (bf.dest, + this_file.short_path, + other_file.short_path)) + else: + deduped_bundlable_files.append(bf) + path_to_files[bf.dest] = this_file + + return deduped_bundlable_files + + +def _dedupe_files(files): + """Deduplicates files based on their short paths. + + Args: + files: The set of `File`s that should be deduplicated based on their short + paths. + Returns: + The `depset` of `File`s where duplicate short paths have been removed by + arbitrarily removing all but one from the set. + """ + short_path_to_files_mapping = {} + + for f in files: + short_path = f.short_path + if short_path not in short_path_to_files_mapping: + short_path_to_files_mapping[short_path] = f + + return depset(short_path_to_files_mapping.values()) + + +def _dedupe_resource_set_files(resource_set): + """Deduplicates the files in a resource set based on their short paths. + + It is possible to have genrules that produce outputs that will be used later + as resource inputs to other rules (and not just genrules, in fact, but any + rule that produces an output file), and these rules register separate actions + for each split configuration when a target is built for multiple + architectures. If we don't deduplicate those files, the outputs of both sets + of actions will be sent to the resource processor and it will attempt to put + the compiled results in the same intermediate file location. + + Therefore, we deduplicate resources that have the same short path, which + ensures (due to action pruning) that only one set of actions will be executed + and only one output will be generated. This implies that the genrule must + produce equivalent content for each configuration. This is likely OK, because + if the output is actually architecture-dependent, then the actions need to + produce those outputs with names that allow the bundler to distinguish them. + + Args: + resource_set: The resource set whose `infoplists`, `resources`, + `structured_resources`, and `structured_resource_zips` should be + deduplicated. + Returns: + A new resource set with duplicate files removed. + """ + return AppleResourceSet( + bundle_dir=resource_set.bundle_dir, + infoplists=_dedupe_files(resource_set.infoplists), + objc_bundle_imports=_dedupe_files(resource_set.objc_bundle_imports), + resources=_dedupe_files(resource_set.resources), + structured_resources=_dedupe_files(resource_set.structured_resources), + structured_resource_zips=_dedupe_files( + resource_set.structured_resource_zips), + swift_module=resource_set.swift_module, + ) + + +def _safe_files(ctx, name): + """Safely returns files from an attribute, or the empty set. + + Args: + ctx: The Skylark context. + name: The attribute name. + Returns: + The `depset` of `File`s if the attribute exists, or an empty set otherwise. + """ + return depset(getattr(ctx.files, name, [])) + + +def _is_ipa(ctx): + """Returns a value indicating whether the target is an IPA. + + This function returns True for "releasable" artifacts that are archived as + IPAs, such as iOS and tvOS applications. It returns False for "intermediate" + bundles, like iOS extensions or watchOS applications (which must be embedded + in an iOS application). + + Args: + ctx: The Skylark context. + Returns: + True if the target is archived as an IPA, or False if it is archived as a + ZIP. + """ + return ctx.outputs.archive.basename.endswith(".ipa") + + +def _create_unprocessed_archive(ctx, + bundle_name, + bundle_path_in_archive, + bundle_merge_files, + bundle_merge_zips, + root_merge_zips, + mnemonic, + progress_description): + """Creates an archive containing the not-yet-signed bundle. + + This function registers an action that uses the underlying bundler.py tool to + build an archive with the bundle contents, before the post-processing script + is run (if present) and before it is signed. This is done because creating + a ZIP in this way turns out to be much faster than performing a large number + of small file copies (for targets with many resources). + + Args: + ctx: The Skylark context. + bundle_name: The name of the bundle. + bundle_path_in_archive: The path to the bundle within the archive. + bundle_merge_files: A list of bundlable file values that represent files + that should be copied to specific locations in the bundle. + bundle_merge_zips: A list of bundlable file values that represent ZIP + archives that should be expanded into specific locations in the bundle. + root_merge_zips: A list of bundlable file values that represent ZIP + archives that should be expanded into specific locations relative to + the root of the archive. + mnemonic: The mnemonic to use for the bundling action. + progress_description: The message that should be shown as the progress + description for the bundling action. + Returns: + A `File` representing the unprocessed archive. + """ + unprocessed_archive = file_support.intermediate( + ctx, "%{name}.unprocessed.zip") + + control = struct( + bundle_merge_files=_bundlable_files_for_control(bundle_merge_files), + bundle_merge_zips=_bundlable_files_for_control(bundle_merge_zips), + bundle_path=bundle_path_in_archive, + output=unprocessed_archive.path, + root_merge_zips=_bundlable_files_for_control(root_merge_zips), + ) + control_file = file_support.intermediate(ctx, "%{name}.bundler-control") + ctx.file_action( + output=control_file, + content=control.to_json() + ) + + bundler_py = ctx.file._bundler_py + bundler_inputs = ( + list(bundling_support.bundlable_file_sources( + bundle_merge_files + bundle_merge_zips + root_merge_zips)) + + [bundler_py, control_file] + ) + + ctx.action( + inputs=bundler_inputs, + outputs=[unprocessed_archive], + command=[ + "/bin/bash", "-c", + "python2.7 %s %s" % (bundler_py.path, control_file.path), + ], + mnemonic=mnemonic, + progress_message="Bundling %s: %s" % (progress_description, bundle_name) + ) + return unprocessed_archive + + +def _process_and_sign_archive(ctx, + bundle_name, + bundle_path_in_archive, + output_archive, + unprocessed_archive, + mnemonic, + progress_description): + """Post-processes and signs an archived bundle. + + Args: + ctx: The Skylark context. + bundle_name: The name of the bundle. + bundle_path_in_archive: The path to the bundle inside the archive. + output_archive: The `File` representing the processed and signed archive. + unprocessed_archive: The `File` representing the archive containing the + bundle that has not yet been processed or signed. + mnemonic: The mnemonic to use for the bundling action. + progress_description: The message that should be shown as the progress + description for the bundling action. + Returns: + The path to the directory that represents the root of the expanded + processed and signed files (before zipping). This is useful to external + tools that want to access the directory directly instead of unzipping the + final archive again. + """ + script_inputs = [unprocessed_archive] + + entitlements = None + if hasattr(ctx.file, "entitlements") and ctx.file.entitlements: + entitlements = ctx.file.entitlements + script_inputs.append(entitlements) + + signing_command_lines = "" + if not ctx.attr._skip_signing: + signing_command_lines = codesigning_support.signing_command_lines( + ctx, bundle_path_in_archive, entitlements) + + post_processor = ctx.executable.ipa_post_processor + post_processor_path = "" + if post_processor: + post_processor_path = post_processor.path + script_inputs.append(post_processor) + + # The directory where the archive contents will be collected. This path is + # also passed out via the apple_bundle provider so that external tools can + # access the bundle layout directly, saving them an extra unzipping step. + work_dir = remove_extension(output_archive.path) + ".archive-root" + + # Only compress the IPA for optimized (release) builds. For debug builds, + # zip without compression, which will speed up the build. + should_compress = (ctx.var["COMPILATION_MODE"] == "opt") + + process_and_sign_script = file_support.intermediate( + ctx, "%{name}.process-and-sign.sh") + ctx.template_action( + template=ctx.file._process_and_sign_template, + output=process_and_sign_script, + executable=True, + substitutions={ + "%output_path%": output_archive.path, + "%post_processor%": post_processor_path or "", + "%signing_command_lines%": signing_command_lines, + "%should_compress%": "1" if should_compress else "", + "%unprocessed_archive_path%": unprocessed_archive.path, + "%work_dir%": work_dir, + }, + ) + + platform_support.xcode_env_action( + ctx, + inputs=script_inputs, + outputs=[output_archive], + executable=process_and_sign_script, + mnemonic=mnemonic + "ProcessAndSign", + progress_message="Processing and signing %s: %s" % ( + progress_description, bundle_name) + ) + return work_dir + + +def _farthest_directory_matching(path, extension): + """Returns the part of a path with the given extension closest to the root. + + For example, if `path` is `"foo/bar.bundle/baz.bundle"`, passing `".bundle"` + as the extension will return `"foo/bar.bundle"`. + + Args: + path: The path. + extension: The extension of the directory to find. + Returns: + The portion of the path that ends in the given extension that is closest + to the root of the path. + """ + prefix, ext, _ = path.partition("." + extension) + if ext: + return prefix + ext + + fail("Expected path %r to contain %r, but it did not" % ( + path, "." + extension)) + + +def _run( + ctx, + mnemonic, + progress_description, + bundle_id, + additional_bundlable_files=depset(), + additional_resources=depset(), + embedded_bundles=[], + framework_files=depset(), + is_dynamic_framework=False): + """Implements the core bundling logic for an Apple bundle archive. + + Args: + ctx: The Skylark context. Required. + mnemonic: The mnemonic to use for the final bundling action. Required. + progress_description: The human-readable description of the bundle being + created in the progress message. For example, in the progress message + "Bundling iOS application: ", the string passed into this + argument would be "iOS application". Required. + bundle_id: Bundle identifier to set to the bundle. Required. + additional_bundlable_files: An optional list of additional bundlable files + that should be copied into the final bundle at locations denoted by + their bundle path. + additional_resources: An optional set of additional resources not included + by dependencies that should also be passed to the resource processing + logic (for example, app icons, launch images, and launch storyboards). + embedded_bundles: A list of values (as returned by + `bundling_support.embedded_bundle`) that denote bundles such as + extensions or frameworks that should be included in the bundle being + built. + framework_files: An optional set of bundlable files that should be copied + into the framework that this rule produces. If any files are present, + this is implicitly noted to be a framework bundle, and additional + provider keys (such as framework search paths) will be propagated + appropriately. + is_dynamic_framework: If True, create this bundle as a dynamic framework. + Returns: + A tuple containing two values: + 1. A dictionary with two providers: an `objc` provider containing the + transitive dependencies of the artifacts used by the current rule, and + an `apple_bundle` provider with additional metadata about the bundle + (such as its final Info.plist file). + 2. A set of additional outputs that should be returned by the calling rule. + """ + _validate_attributes(ctx) + + # A list of additional implicit outputs that should be returned by the + # calling rule. + additional_outputs = [] + + # The name of the target is used as the name of the executable in the binary, + # which we also need to write into the Info.plist file over whatever the user + # already has there. + bundle_name = bundling_support.bundle_name(ctx) + + # bundle_merge_files collects the files (or directories of files) from + # providers and actions that should be copied into the bundle by the final + # packaging action. + bundle_merge_files = [ + _convert_native_bundlable_file( + bf, bundle_dir=bundling_support.path_in_contents_dir(ctx, "") + ) for bf in additional_bundlable_files + ] + + # bundle_merge_zips collects ZIP files from providers and actions that should + # be expanded into the bundle by the final packaging action. + bundle_merge_zips = [] + + # Collects the ZIP files that should be merged into the root of an archive. + # archive. Note that this only applies if an IPA is being built; it is + # ignored for ZIP archives created from non-app artifacts like extensions. + root_merge_zips = [] + + # Collects ZIP files representing frameworks that should be propagated to the + # bundle inside which the current bundle is embedded. + propagated_framework_zips = [] + + # Keeps track of whether this is a device build or a simulator build. + is_device = platform_support.is_device_build(ctx) + + # If this is a device build for which code signing is required, copy the + # provisioning profile into the bundle with the expected name. + provisioning_profile = getattr(ctx.file, "provisioning_profile", None) + if (is_device and provisioning_profile and not ctx.attr._skip_signing): + bundle_merge_files.append(bundling_support.contents_file( + ctx, provisioning_profile, + codesigning_support.embedded_provisioning_profile_name(ctx))) + + # The path to the .app bundle inside the IPA archive. + bundle_path_in_archive = (ctx.attr._path_in_archive_format % + bundling_support.bundle_name_with_extension(ctx)) + + # Start by collecting resources for the bundle being built. The empty string + # for the bundle path indicates that resources should appear at the top level + # of the bundle. + target_infoplists = list(_safe_files(ctx, "infoplists")) + + bundle_dirs = [] + resource_sets = [] + + if (hasattr(ctx.attr, "exclude_resources") and ctx.attr.exclude_resources): + resource_sets.append(AppleResourceSet(infoplists=target_infoplists)) + else: + # Collect the set of bundle_dirs from all framework dependencies, which + # will be used to deduplicate resources (by excluding them from the + # depending app/extension). + framework_bundle_dirs = depset() + if hasattr(ctx.attr, "frameworks"): + for framework in getattr(ctx.attr, "frameworks", []): + if hasattr(framework, "bundle_dirs"): + for bundle_dir in framework.bundle_dirs.bundle_dirs: + framework_bundle_dirs = framework_bundle_dirs | [bundle_dir] + if ctx.attr._propagates_frameworks: + propagated_framework_zips.append(framework.apple_bundle.archive) + + # Add the transitive resource sets, except for those that have already been + # included by a framework dependency. + apple_resource_providers = provider_support.binary_or_deps_providers( + ctx, "AppleResource") + for p in apple_resource_providers: + for rs in p.resource_sets: + # Don't propagate empty bundle directory, as this is indicative of + # root-level resources. + # TODO(b/36512244): Implement root-level resource deduping. + if rs.bundle_dir: + bundle_dirs.append(rs.bundle_dir) + bundle_dir_or_empty = rs.bundle_dir or "" + # TODO(b/36512244): Ensure that there aren't two differing bundles + # with different names, as one would get silently dropped. + if bundle_dir_or_empty not in framework_bundle_dirs: + resource_sets.append(rs) + + # Finally, add any extra resources specific to the target being built + # itself. + target_resources = ( + depset(additional_resources) + _safe_files(ctx, "strings")) + resource_sets.append(AppleResourceSet( + infoplists=target_infoplists, + resources=target_resources, + )) + + # We need to keep track of the Info.plist for the main bundle so that it can + # be propagated out (for situations where this bundle is a child of another + # bundle and bundle ID consistency is checked). + main_infoplist = None + + # Collects the compiled storyboard artifacts for later linking. + compiled_storyboards = [] + + # Iterate over each set of resources and register the actions. This + # ensures that each bundle among the dependencies has its resources + # processed independently. + resource_sets = [_dedupe_resource_set_files(rs) + for rs in apple_resource_set_utils.minimize(resource_sets)] + for r in resource_sets: + ri = resource_support.resource_info(bundle_id, bundle_dir=r.bundle_dir) + process_result = resource_actions.process_resources(ctx, r.resources, ri) + compiled_storyboards.extend(list(process_result.compiled_storyboards)) + bundle_merge_files.extend(list(process_result.bundle_merge_files)) + bundle_merge_zips.extend(list(process_result.bundle_merge_zips)) + + # Merge structured resources verbatim, preserving their owner-relative + # paths. + if r.objc_bundle_imports: + # TODO(b/33618143): Remove the ugly special case for objc_bundle. + bundle_merge_files.extend([ + bundling_support.resource_file( + ctx, f, optionally_prefixed_path( + relativize_path( + f.short_path, + _farthest_directory_matching(f.short_path, "bundle")), + r.bundle_dir)) + for f in r.objc_bundle_imports + ]) + + bundle_merge_files.extend([ + bundling_support.resource_file( + ctx, f, optionally_prefixed_path( + relativize_path(f.short_path, f.owner.package), + r.bundle_dir)) + for f in r.structured_resources + ]) + bundle_merge_zips.extend([ + bundling_support.resource_file(ctx, f, r.bundle_dir) + for f in r.structured_resource_zips + ]) + + # Merge the plists into binary format and collect the resulting PkgInfo + # file as well. + infoplists = list(r.infoplists) + list(process_result.partial_infoplists) + if infoplists: + merge_infoplist_args = { + "input_plists": list(infoplists), + "bundle_id": bundle_id, + } + + # Compare to child plists (i.e., from extensions and nested binaries) + # only if we're processing the main bundle and not a resource bundle. + if not r.bundle_dir: + child_infoplists = [ + eb.apple_bundle.infoplist for eb in embedded_bundles + if eb.verify_bundle_id + ] + merge_infoplist_args["child_plists"] = child_infoplists + merge_infoplist_args["executable_bundle"] = True + + plist_results = plist_actions.merge_infoplists( + ctx, r.bundle_dir, **merge_infoplist_args) + + if not r.bundle_dir: + main_infoplist = plist_results.output_plist + + # The files below need to be merged with specific names in the final + # bundle. + bundle_merge_files.append(bundling_support.contents_file( + ctx, plist_results.output_plist, + optionally_prefixed_path("Info.plist", r.bundle_dir))) + + if plist_results.pkginfo: + bundle_merge_files.append(bundling_support.contents_file( + ctx, plist_results.pkginfo, + optionally_prefixed_path("PkgInfo", r.bundle_dir))) + + # Compile any storyboards and create the script that runs ibtool again + # during the bundling stage to link them. This is necessary to resolve + # references between different storyboards, and to copy the results to the + # correct location in the bundle in a platform-agnostic way; for example, + # storyboards in watchOS applications don't retain the .storyboardc folder. + if compiled_storyboards: + linked_storyboards = resource_actions.ibtool_link( + ctx, compiled_storyboards) + bundle_merge_zips.append(bundling_support.resource_file( + ctx, linked_storyboards, ".")) + + # Some application/extension types require stub executables, so collect that + # information if necessary. + product_info = product_support.product_type_info_for_target(ctx) + if product_info: + # We explicitly don't use bash_quote here because we don't want to escape + # the environment variable substitutions. + stub_binary = product_actions.copy_stub_for_bundle(ctx, product_info) + bundle_merge_files.append(bundling_support.binary_file( + ctx, stub_binary, bundle_name, executable=True)) + if product_info.bundle_path: + # TODO(b/34684393): Figure out if macOS ever uses stub binaries for any + # product types, and if so, is this the right place for them? + bundle_merge_files.append(bundling_support.contents_file( + ctx, stub_binary, product_info.bundle_path, executable=True)) + # TODO(b/34047985): This should be conditioned on a flag, not just + # compilation mode. + if ctx.var["COMPILATION_MODE"] == "opt": + support_zip = product_actions.create_stub_zip_for_archive_merging( + ctx, product_info) + root_merge_zips.append(bundling_support.bundlable_file(support_zip, ".")) + elif hasattr(ctx.file, "binary"): + if not ctx.file.binary: + fail("Library dependencies must be provided for this product type.") + bundle_merge_files.append(bundling_support.binary_file( + ctx, ctx.file.binary, bundle_name, executable=True)) + + # Compute the Swift libraries that are used by the target currently being + # built. + if swift_support.uses_swift(ctx): + swift_zip = swift_actions.zip_swift_dylibs(ctx) + + if ctx.attr._propagates_frameworks: + propagated_framework_zips.append(swift_zip) + else: + bundle_merge_zips.append(bundling_support.contents_file( + ctx, swift_zip, "Frameworks")) + + platform, _ = platform_support.platform_and_sdk_version(ctx) + root_merge_zips.append(bundling_support.bundlable_file( + swift_zip, "SwiftSupport/%s" % platform.name_in_plist.lower())) + + # Include any embedded bundles. + for eb in embedded_bundles: + apple_bundle = eb.apple_bundle + if ctx.attr._propagates_frameworks: + propagated_framework_zips += list(apple_bundle.propagated_framework_zips) + propagated_framework_files += list(apple_bundle.propagated_framework_files) + else: + if apple_bundle.propagated_framework_zips: + bundle_merge_zips.extend([ + bundling_support.contents_file(ctx, f, "Frameworks") + for f in apple_bundle.propagated_framework_zips + ]) + if apple_bundle.propagated_framework_files: + bundle_merge_files.extend(_bundlable_dynamic_framework_files( + ctx, apple_bundle.propagated_framework_files)) + + bundle_merge_zips.append(bundling_support.contents_file( + ctx, apple_bundle.archive, eb.path)) + root_merge_zips.extend(list(apple_bundle.root_merge_zips)) + + # Merge in any prebuilt frameworks (i.e., objc_framework dependencies). + objc_providers = provider_support.binary_or_deps_providers(ctx, "objc") + propagated_framework_files = [] + for objc in objc_providers: + files = objc.dynamic_framework_file + if ctx.attr._propagates_frameworks: + propagated_framework_files.extend(list(files)) + else: + bundle_merge_files.extend(_bundlable_dynamic_framework_files(ctx, files)) + + bundle_merge_files = _dedupe_bundle_merge_files(bundle_merge_files) + + # Perform the final bundling tasks. + root_merge_zips_to_archive = root_merge_zips if _is_ipa(ctx) else [] + unprocessed_archive = _create_unprocessed_archive( + ctx, bundle_name, bundle_path_in_archive, bundle_merge_files, + bundle_merge_zips, root_merge_zips_to_archive, mnemonic, + progress_description) + work_dir = _process_and_sign_archive( + ctx, bundle_name, bundle_path_in_archive, ctx.outputs.archive, + unprocessed_archive, mnemonic, progress_description) + + additional_providers = {} + + # Create a .dSYM bundle with the expected name next to the .ipa in the output + # directory. We still have to check for the existence of the AppleDebugOutputs + # provider because some binary rules, such as apple_static_library, do not + # propagate it. + if (ctx.fragments.objc.generate_dsym and + getattr(ctx.attr, "binary", None) and + apple_common.AppleDebugOutputs in ctx.attr.binary): + additional_outputs.extend(dsym_actions.create_symbol_bundle(ctx)) + additional_providers["AppleDebugOutputs"] = ( + ctx.attr.binary[apple_common.AppleDebugOutputs]) + + objc_provider_args = {} + if framework_files: + framework_dir, bundled_framework_files = ( + _copy_framework_files(ctx, framework_files)) + if is_dynamic_framework: + objc_provider_args["dynamic_framework_dir"] = depset([framework_dir]) + objc_provider_args["dynamic_framework_file"] = bundled_framework_files + else: + objc_provider_args["framework_dir"] = depset([framework_dir]) + objc_provider_args["static_framework_file"] = bundled_framework_files + + objc_provider_args["providers"] = objc_providers + + providers = { + "apple_bundle": struct( + archive=ctx.outputs.archive, + archive_root=work_dir, + infoplist=main_infoplist, + propagated_framework_files=depset(propagated_framework_files), + propagated_framework_zips=depset(propagated_framework_zips), + root_merge_zips=root_merge_zips if not _is_ipa(ctx) else [], + uses_swift=swift_support.uses_swift(ctx), + bundle_id=bundle_id, + ), + "objc": apple_common.new_objc_provider(**objc_provider_args), + "bundle_dirs": struct(**{"bundle_dirs": bundle_dirs}) + } + for key, provider in additional_providers.items(): + providers[key] = provider + + return providers, depset(additional_outputs) + + +def _copy_framework_files(ctx, framework_files): + """Copies the files in `framework_files` to the right place in the framework. + + Args: + ctx: The Skylark context. + framework_files: A list of files to copy into the framework. + Returns: + A two-element tuple: the framework directory path, and a set containing the + output files in their final locations. + """ + bundle_name = bundling_support.bundle_name(ctx) + framework_dir_name = "_frameworks/" + bundle_name + ".framework/" + bundled_framework_files = [] + for framework_file in framework_files: + output_file = ctx.new_file( + framework_dir_name + framework_file.bundle_path) + ctx.action( + outputs=[output_file], + inputs=[framework_file.file], + mnemonic="Cp", + arguments=[ + output_file.dirname, framework_file.file.path, output_file.path], + command='mkdir -p "$1" && cp "$2" "$3"', + progress_message=( + "Copying " + framework_file.file.path + " to " + output_file.path) + ) + bundled_framework_files.append(output_file) + return (ctx.outputs.archive.dirname + "/" + framework_dir_name, + depset(bundled_framework_files)) + + +# Define the loadable module that lists the exported symbols in this file. +bundler = struct( + run=_run, +) diff --git a/apple/bundling/bundler.py b/apple/bundling/bundler.py new file mode 100644 index 0000000000..d6676be9d4 --- /dev/null +++ b/apple/bundling/bundler.py @@ -0,0 +1,219 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""File-system bundling logic for Apple bundles. + +The bundler takes a set of files and merges them into an uncompressed ZIP file, +without building the actual file/directory structure for the bundle on the file +system. This greatly speeds up the bundling process when a large number of +resources are used, because it avoids performing a lot of small file copies. + +This script takes a single argument that points to a file containing the JSON +representation of a "control" structure, which makes it easier to pass in +complex structured data. This control structure is a dictionary with the +following keys: + + bundle_path: The path relative to the archive root where the bundle files will + be stored. Application targets, for example, might specify a path like + "Payload/foo.app". + bundle_merge_files: A list of dictionaries representing files to be merged + into the bundle. Each dictionary contains the following fields: "src", the + path of the file to be added to the bundle; "dest", the path inside the + bundle where the file should live, including its filename (which lets the + name be changed, if desired); and "executable", a Boolean value indicating + whether or not the executable bit should be set on the file. If + `executable` is omitted, False is used. + The destination path is relative to `bundle_path`. + bundle_merge_zips: A list of dictionaries representing ZIP archives whose + contents should be merged into the bundle. Each dictionary contains two + fields: "src", the path of the archive whose contents should be merged + into the bundle; and "dest", the path inside the bundle where the ZIPs + contents should be placed. The destination path is relative to + `bundle_path`. + output: The path to the uncompressed ZIP archive that should be created with + the merged bundle contents. + root_merge_zips: A list of dictionaries representing the ZIP archives whose + contents should be merged into the archive at the root. Each dictionary + contains two fields: "src", the path of the archive whose contents should + be merged into the archive; and "dest", the path inside the archive where + the ZIPs contents should be placed. This is used for support files, such + as Swift libraries and watchOS stub executables, that must be shipped to + Apple at the root of the archive as well as within the bundle itself. +""" + +import json +import md5 +import os +import sys +import zipfile + +BUNDLE_CONFLICT_MSG_TEMPLATE = ( + 'Cannot place two files at the same location %r in the archive') + + +class BundleConflictError(ValueError): + """Raised when two different files would be bundled in the same location.""" + + def __init__(self, dest): + """Initializes an error with the given key and values. + + Args: + dest: The destination path inside the archive. + """ + self.dest = dest + ValueError.__init__(self, BUNDLE_CONFLICT_MSG_TEMPLATE % dest) + + +class Bundler(object): + """Implements the core functionality of the bundler.""" + + def __init__(self, control): + """Initializes Bundler with the given control options. + + Args: + control: The dictionary of options used to control the tool. Please see + the moduledoc for a description of the format of this dictionary. + """ + self._control = control + + # Keep track of hashes of each entry; this will be faster than pulling the + # data back out of the archive as it's written. + self._entry_hashes = {} + + def run(self): + """Performs the operations requested by the control struct.""" + output_path = self._control.get('output') + if not output_path: + raise ValueError('No output file specified.') + + bundle_path = self._control.get('bundle_path', '') + bundle_merge_files = self._control.get('bundle_merge_files', []) + bundle_merge_zips = self._control.get('bundle_merge_zips', []) + root_merge_zips = self._control.get('root_merge_zips', []) + + with zipfile.ZipFile(output_path, 'w') as out_zip: + for z in bundle_merge_zips: + dest = os.path.normpath(os.path.join(bundle_path, z['dest'])) + self._add_zip_contents(z['src'], dest, out_zip) + + for f in bundle_merge_files: + dest = os.path.join(bundle_path, f['dest']) + self._add_files(f['src'], dest, f.get('executable', False), out_zip) + + for z in root_merge_zips: + self._add_zip_contents(z['src'], z['dest'], out_zip) + + def _add_files(self, src, dest, executable, out_zip): + """Adds a file or a directory of files to the ZIP archive. + + Args: + src: The path to the file or directory that should be added. + dest: The path inside the archive where the files should be stored. If + `src` is a single file, then `dest` should include the filename that + the file should have within the archive. If `src` is a directory, it + represents the directory into which the files underneath `src` will + be recursively added. + executable: A Boolean value indicating whether or not the file(s) should + be made executable. + out_zip: The `ZipFile` into which the files should be added. + """ + if os.path.isdir(src): + for root, _, files in os.walk(src): + relpath = os.path.relpath(root, src) + for filename in files: + fsrc = os.path.join(root, filename) + fdest = os.path.normpath(os.path.join(dest, relpath, filename)) + with open(fsrc, 'r') as f: + self._write_entry(fdest, f.read(), executable, out_zip) + elif os.path.isfile(src): + with open(src, 'r') as f: + self._write_entry(dest, f.read(), executable, out_zip) + + def _add_zip_contents(self, src, dest, out_zip): + """Adds the contents of another ZIP file to the output ZIP archive. + + Args: + src: The path to the ZIP file whose contents should be added. + dest: The path inside the output archive where the contents of `src` + should be expanded. The directory structure of `src` is preserved + underneath this path. + out_zip: The `ZipFile` into which the files should be added. + """ + with zipfile.ZipFile(src, 'r') as src_zip: + for src_zipinfo in src_zip.infolist(): + # Normalize the destination path to remove any extraneous internal + # slashes or "." segments, but retain the final slash for directory + # entries. + file_dest = os.path.normpath(os.path.join(dest, src_zipinfo.filename)) + if src_zipinfo.filename.endswith('/'): + file_dest += '/' + + # Check for Unix --x--x--x permissions. + executable = src_zipinfo.external_attr >> 16L & 0111 != 0 + data = src_zip.read(src_zipinfo) + self._write_entry(file_dest, data, executable, out_zip) + + def _write_entry(self, dest, data, executable, out_zip): + """Writes the given data as a file in the output ZIP archive. + + Args: + dest: The path inside the archive where the data should be written. + data: The data to be written in the archive. + executable: A Boolean value indicating whether or not the file should be + made executable. + out_zip: The `ZipFile` into which the files should be added. + Raises: + BundleConflictError: If two files with different content would be placed + at the same location in the ZIP file. + """ + new_hash = md5.new(data).digest() + existing_hash = self._entry_hashes.get(dest) + if existing_hash: + if existing_hash == new_hash: + return + raise BundleConflictError(dest) + + self._entry_hashes[dest] = new_hash + + zipinfo = zipfile.ZipInfo(dest) + zipinfo.compress_type = zipfile.ZIP_STORED + + if dest.endswith('/'): + # Unix rwxr-xr-x permissions and S_IFDIR (directory) on the left side of + # the bitwise-OR; MS-DOS directory flag on the right. + zipinfo.external_attr = 0040755 << 16L | 0x10 + else: + # Unix rw-r--r-- permissions and S_IFREG (regular file). + zipinfo.external_attr = 0100644 << 16L + if executable: + # Add Unix --x--x--x permissions. + zipinfo.external_attr |= 0111 << 16L + + out_zip.writestr(zipinfo, data) + + +def _main(control_path): + with open(control_path) as control_file: + control = json.load(control_file) + + bundler = Bundler(control) + bundler.run() + + +if __name__ == '__main__': + if len(sys.argv) < 2: + sys.stderr.write('ERROR: Path to control file not specified.\n') + exit(1) + + _main(sys.argv[1]) diff --git a/apple/bundling/bundler_unittest.py b/apple/bundling/bundler_unittest.py new file mode 100644 index 0000000000..352f89f907 --- /dev/null +++ b/apple/bundling/bundler_unittest.py @@ -0,0 +1,298 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for Bundler.""" + +import os +import shutil +import StringIO +import tempfile +import unittest +import zipfile + +import bundler + + +def _run_bundler(control): + """Helper function that runs Bundler with the given control struct. + + This function inserts a StringIO object as the control's "output" key and + returns it after bundling; this object will contain the binary data for the + ZIP file that was created, which can then be reopened and tested. + + Args: + control: The control struct to pass to Bundler. See the module doc for + the bundler module for a description of this format. + Returns: + The StringIO object containing the binary data for a bundled ZIP file. + """ + output = StringIO.StringIO() + control['output'] = output + + tool = bundler.Bundler(control) + tool.run() + + return output + + +class BundlerTest(unittest.TestCase): + + def setUp(self): + self._scratch_dir = tempfile.mkdtemp('bundlerTestScratch') + + def tearDown(self): + shutil.rmtree(self._scratch_dir) + + def _scratch_file(self, name, content=''): + """Creates a scratch file with the given name. + + The scratch file's path, which is returned by this function, can then be + passed into the bundler as one of its `bundle_merge_files`. + + Args: + name: The name of the file. + content: The content to write into the file. The default is empty. + Returns: + The absolute path to the file. + """ + path = os.path.join(self._scratch_dir, name) + dirname = os.path.dirname(path) + if not os.path.isdir(dirname): + os.makedirs(dirname) + + with open(path, 'w') as f: + f.write(content) + return path + + def _scratch_zip(self, name, *entries): + """Creates a scratch ZIP file with the given entries. + + The scratch ZIP's path, which is returned by this function, can then be + passed into the bunlder as one of its `bundle_merge_zips` or + `root_merge_zips`. + + Args: + name: The name of the ZIP file. + *entries: A list of archive-relative paths that will represent empty + files in the ZIP. If a path entry begins with a "*", it will be made + executable. If a path entry contains a colon, the text after the + colon will be used as the content of the file. + Returns: + The absolute path to the ZIP file. + """ + path = os.path.join(self._scratch_dir, name) + with zipfile.ZipFile(path, 'w') as z: + for entry in entries: + executable = entry.startswith('*') + entry_without_content, _, content = entry.partition(':') + + zipinfo = zipfile.ZipInfo(entry_without_content.rpartition('*')[-1]) + zipinfo.compress_type = zipfile.ZIP_STORED + # Unix rw-r--r-- permissions and S_IFREG (regular file). + zipinfo.external_attr = 0100644 << 16L + if executable: + zipinfo.external_attr = 0111 << 16L + z.writestr(zipinfo, content) + return path + + def _assert_zip_contains(self, zip_file, entry, executable=False): + """Asserts that a `ZipFile` has an entry with the given path. + + This is a convenience function that catches the `KeyError` that would be + raised if the entry was not found and turns it into a test failure. + + Args: + zip_file: The `ZipFile` object. + entry: The archive-relative path to verify. + executable: The expected value of the executable bit (True or False). + """ + try: + zipinfo = zip_file.getinfo(entry) + if executable: + self.assertEquals( + 0111, zipinfo.external_attr >> 16L & 0111, + 'Expected %r to be executable, but it was not' % entry) + else: + self.assertEquals( + 0, zipinfo.external_attr >> 16L & 0111, + 'Expected %r not to be executable, but it was' % entry) + except KeyError: + self.fail('Bundled ZIP should have contained %r, but it did not' % entry) + + def test_bundle_merge_files(self): + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_files': [ + {'src': self._scratch_file('foo.txt'), 'dest': 'foo.txt'}, + {'src': self._scratch_file('bar.txt'), 'dest': 'bar.txt'}, + ] + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/foo.txt') + self._assert_zip_contains(z, 'Payload/foo.app/bar.txt') + + def test_bundle_merge_files_with_executable(self): + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_files': [ + {'src': self._scratch_file('foo.exe'), 'dest': 'foo.exe', + 'executable': True}, + {'src': self._scratch_file('bar.txt'), 'dest': 'bar.txt', + 'executable': False}, + ] + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/foo.exe', True) + self._assert_zip_contains(z, 'Payload/foo.app/bar.txt', False) + + def test_bundle_merge_files_with_renaming(self): + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_files': [ + {'src': self._scratch_file('foo.txt'), 'dest': 'renamed1'}, + {'src': self._scratch_file('bar.txt'), 'dest': 'renamed2'}, + ] + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/renamed1') + self._assert_zip_contains(z, 'Payload/foo.app/renamed2') + + def test_bundle_merge_files_with_directories(self): + a_txt = self._scratch_file('a.txt') + root = os.path.dirname(a_txt) + self._scratch_file('b.txt') + self._scratch_file('c/d.txt') + self._scratch_file('c/e/f.txt') + + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_files': [{'src': root, 'dest': 'x/y/z'}], + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/x/y/z/a.txt') + self._assert_zip_contains(z, 'Payload/foo.app/x/y/z/b.txt') + self._assert_zip_contains(z, 'Payload/foo.app/x/y/z/c/d.txt') + self._assert_zip_contains(z, 'Payload/foo.app/x/y/z/c/e/f.txt') + + def test_bundle_merge_zips(self): + foo_zip = self._scratch_zip('foo.zip', + 'foo.bundle/img.png', 'foo.bundle/strings.txt') + bar_zip = self._scratch_zip('bar.zip', + 'bar.bundle/img.png', 'bar.bundle/strings.txt') + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_zips': [ + {'src': foo_zip, 'dest': '.'}, + {'src': bar_zip, 'dest': '.'}, + ] + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/foo.bundle/img.png') + self._assert_zip_contains(z, 'Payload/foo.app/foo.bundle/strings.txt') + self._assert_zip_contains(z, 'Payload/foo.app/bar.bundle/img.png') + self._assert_zip_contains(z, 'Payload/foo.app/bar.bundle/strings.txt') + + def test_bundle_merge_zips_propagates_executable(self): + foo_zip = self._scratch_zip('foo.zip', '*foo.bundle/some.exe') + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_zips': [{'src': foo_zip, 'dest': '.'}], + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/foo.bundle/some.exe', True) + + def test_root_merge_zips(self): + support_zip = self._scratch_zip('support.zip', 'SomeSupport/some.dylib') + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'root_merge_zips': [{'src': support_zip, 'dest': '.'}], + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'SomeSupport/some.dylib') + + def test_root_merge_zips_with_different_destination(self): + support_zip = self._scratch_zip('support.zip', 'some.dylib') + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'root_merge_zips': [{'src': support_zip, 'dest': 'SomeSupport'}], + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'SomeSupport/some.dylib') + + def test_root_merge_zips_propagates_executable(self): + support_zip = self._scratch_zip('support.zip', '*SomeSupport/some.dylib') + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'root_merge_zips': [{'src': support_zip, 'dest': '.'}], + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'SomeSupport/some.dylib', True) + + def test_duplicate_files_with_same_content_are_allowed(self): + foo_txt = self._scratch_file('foo.txt', 'foo') + bar_txt = self._scratch_file('bar.txt', 'foo') + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_files': [ + {'src': foo_txt, 'dest': 'renamed'}, + {'src': bar_txt, 'dest': 'renamed'}, + ] + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/renamed') + + def test_duplicate_files_with_different_content_raise_error(self): + foo_txt = self._scratch_file('foo.txt', 'foo') + bar_txt = self._scratch_file('bar.txt', 'bar') + with self.assertRaisesRegexp( + bundler.BundleConflictError, + bundler.BUNDLE_CONFLICT_MSG_TEMPLATE % 'Payload/foo.app/renamed'): + _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_files': [ + {'src': foo_txt, 'dest': 'renamed'}, + {'src': bar_txt, 'dest': 'renamed'}, + ] + }) + + def test_zips_with_duplicate_files_but_same_content_are_allowed(self): + one_zip = self._scratch_zip('one.zip', 'some.dylib:foo') + two_zip = self._scratch_zip('two.zip', 'some.dylib:foo') + out_zip = _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_zips': [ + {'src': one_zip, 'dest': '.'}, + {'src': two_zip, 'dest': '.'}, + ] + }) + with zipfile.ZipFile(out_zip, 'r') as z: + self._assert_zip_contains(z, 'Payload/foo.app/some.dylib') + + def test_zips_with_duplicate_files_and_different_content_raise_error(self): + one_zip = self._scratch_zip('one.zip', 'some.dylib:foo') + two_zip = self._scratch_zip('two.zip', 'some.dylib:bar') + with self.assertRaisesRegexp( + bundler.BundleConflictError, + bundler.BUNDLE_CONFLICT_MSG_TEMPLATE % 'Payload/foo.app/some.dylib'): + _run_bundler({ + 'bundle_path': 'Payload/foo.app', + 'bundle_merge_zips': [ + {'src': one_zip, 'dest': '.'}, + {'src': two_zip, 'dest': '.'}, + ] + }) + + +if __name__ == '__main__': + unittest.main() diff --git a/apple/bundling/bundling_support.bzl b/apple/bundling/bundling_support.bzl new file mode 100644 index 0000000000..1ec735436e --- /dev/null +++ b/apple/bundling/bundling_support.bzl @@ -0,0 +1,248 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Low-level bundling name helpers.""" + + +def _binary_file(ctx, src, dest, executable=False): + """Returns a bundlable file whose destination is in the binary directory. + + Args: + ctx: The Skylark context. + src: The `File` artifact that should be bundled. + dest: The path within the bundle's binary directory where the file should + be placed. + executable: True if the file should be made executable. + Returns: + A bundlable file struct (see `bundling_support.bundlable_file`). + """ + return _bundlable_file(src, _path_in_binary_dir(ctx, dest), executable) + + +def _bundlable_file(src, dest, executable=False): + """Returns a value that represents a bundlable file or ZIP archive. + + A "bundlable file" is a struct that maps a file (`"src"`) to a path within a + bundle (`"dest"`). This can be used with plain files, where `dest` denotes + the path within the bundle where the file should be placed (including its + filename, which allows it to be changed), or with ZIP archives, where `dest` + denotes the location within the bundle where the ZIP's contents should be + extracted. + + Args: + src: The `File` artifact that should be bundled. + dest: The path within the bundle where the file should be placed. + executable: True if the file should be made executable. + Returns: + A struct with `src`, `dest`, and `executable` fields representing the + bundlable file. + """ + return struct(src=src, dest=dest, executable=executable) + + +def _bundlable_file_sources(bundlable_files): + """Returns the source files from the given collection of bundlable files. + + This is a convenience function that allows a set of bundlable files to be + quickly turned into a list of files that can be passed to an action's inputs, + for example. + + Args: + bundlable_files: A list or set of bundlable file values (as returned by + `bundling_support.bundlable_file`). + Returns: + A `depset` containing the `File` artifacts from the given bundlable files. + """ + return depset([bf.src for bf in bundlable_files]) + + +def _bundle_name(ctx): + """Returns the name of the bundle. + + Args: + ctx: The Skylark context. + Returns: + The bundle name. + """ + if hasattr(ctx.attr, "_bundle_name_attr"): + bundle_name_attr = ctx.attr._bundle_name_attr + return getattr(ctx.attr, bundle_name_attr) + else: + return ctx.label.name + + +def _bundle_name_with_extension(ctx): + """Returns the name of the bundle with its extension. + + Args: + ctx: The Skylark context. + Returns: + The bundle name with its extension. + """ + return _bundle_name(ctx) + ctx.attr._bundle_extension + + +def _contents_file(ctx, src, dest, executable=False): + """Returns a bundlable file whose destination is in the contents directory. + + Args: + ctx: The Skylark context. + src: The `File` artifact that should be bundled. + dest: The path within the bundle's contents directory where the file should + be placed. + executable: True if the file should be made executable. + Returns: + A bundlable file struct (see `bundling_support.bundlable_file`). + """ + return _bundlable_file(src, _path_in_contents_dir(ctx, dest), executable) + + +def _embedded_bundle(path, apple_bundle, verify_bundle_id): + """Returns a value that represents an embedded bundle in another bundle. + + These values are used by the bundler to indicate how dependencies that are + themselves bundles (such as extensions or frameworks) should be bundled in + the application or target that depends on them. + + Args: + path: The relative path within the depender's bundle where the given bundle + should be located. + apple_bundle: The `apple_bundle` provider of the embedded bundle. + verify_bundle_id: If True, the bundler should verify that the bundle + identifier of the depender is a prefix of the bundle identifier of the + embedded bundle. + Returns: + A struct with `path`, `apple_bundle`, and `verify_bundle_id` fields equal + to the values given in the arguments. + """ + return struct( + path=path, apple_bundle=apple_bundle, verify_bundle_id=verify_bundle_id) + + +def _force_settings_bundle_prefix(bundle_file): + """Forces a file's destination to start with "Settings.bundle/". + + If the given file's destination path contains a directory named "*.bundle", + everything up to that point in the path is removed and replaced with + "Settings.bundle". Otherwise, "Settings.bundle/" is prepended to the path. + + Args: + bundle_file: A value from an objc provider's bundle_file field; in other + words, a struct with file and bundle_path fields. + Returns: + A bundlable file struct with the same File object, but whose path has been + transformed to start with "Settings.bundle/". + """ + _, _, path_inside_bundle = bundle_file.bundle_path.rpartition(".bundle/") + new_path = "Settings.bundle/" + path_inside_bundle + return struct(file=bundle_file.file, bundle_path=new_path) + + +def _header_prefix(input_file): + """Sets a file's bundle destination to a "Headers/" subdirectory. + + Args: + input_file: The File to be bundled + Returns: + A bundlable file struct with the same File object, but whose path has been + transformed to start with "Headers/". + """ + new_path = "Headers/" + input_file.basename + return struct(file=input_file, bundle_path=new_path) + + +def _path_in_binary_dir(ctx, path): + """Makes a path relative to where the bundle's binary is stored. + + On iOS/watchOS/tvOS, the binary is placed directly in the bundle's contents + directory (which itself is actually the bundle root). On macOS, the binary is + in a MacOS directory that is inside the bundle's Contents directory. + + Args: + ctx: The Skylark context. + path: The path to make relative to where the bundle's binary is stored. + Returns: + The path, made relative to where the bundle's binary is stored. + """ + return _path_in_contents_dir( + ctx, ctx.attr._bundle_binary_path_format % (path or "")) + + +def _path_in_contents_dir(ctx, path): + """Makes a path relative to where the bundle's contents are stored. + + Contents include files such as: + * A directory of resources (which itself might be flattened into contents) + * A directory for the binary (which might be flattened) + * Directories for Frameworks and PlugIns (extensions) + * The bundle's Info.plist and PkgInfo + * The code signature + + Args: + ctx: The Skylark context. + path: The path to make relative to where the bundle's contents are stored. + Returns: + The path, made relative to where the bundle's contents are stored. + """ + return ctx.attr._bundle_contents_path_format % (path or "") + + +def _path_in_resources_dir(ctx, path): + """Makes a path relative to where the bundle's resources are stored. + + On iOS/watchOS/tvOS, resources are placed directly in the bundle's contents + directory (which itself is actually the bundle root). On macOS, resources are + in a Resources directory that is inside the bundle's Contents directory. + + Args: + ctx: The Skylark context. + path: The path to make relative to where the bundle's resources are stored. + Returns: + The path, made relative to where the bundle's resources are stored. + """ + return _path_in_contents_dir( + ctx, ctx.attr._bundle_resources_path_format % (path or "")) + + +def _resource_file(ctx, src, dest, executable=False): + """Returns a bundlable file whose destination is in the resources directory. + + Args: + ctx: The Skylark context. + src: The `File` artifact that should be bundled. + dest: The path within the bundle's resources directory where the file + should be placed. + executable: True if the file should be made executable. + Returns: + A bundlable file struct (see `bundling_support.bundlable_file`). + """ + return _bundlable_file(src, _path_in_resources_dir(ctx, dest), executable) + + +# Define the loadable module that lists the exported symbols in this file. +bundling_support = struct( + binary_file=_binary_file, + bundlable_file=_bundlable_file, + bundlable_file_sources=_bundlable_file_sources, + bundle_name=_bundle_name, + bundle_name_with_extension=_bundle_name_with_extension, + contents_file=_contents_file, + embedded_bundle=_embedded_bundle, + header_prefix=_header_prefix, + force_settings_bundle_prefix=_force_settings_bundle_prefix, + path_in_binary_dir=_path_in_binary_dir, + path_in_contents_dir=_path_in_contents_dir, + path_in_resources_dir=_path_in_resources_dir, + resource_file=_resource_file, +) diff --git a/apple/bundling/codesigning_support.bzl b/apple/bundling/codesigning_support.bzl new file mode 100644 index 0000000000..ded24b141a --- /dev/null +++ b/apple/bundling/codesigning_support.bzl @@ -0,0 +1,173 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions related to code signing of Apple bundles.""" + +load("//apple/bundling:mock_support.bzl", "mock_support") +load("//apple/bundling:platform_support.bzl", + "platform_support") +load("//apple/bundling:plist_support.bzl", "plist_support") +load("//apple/bundling:swift_support.bzl", "swift_support") +load("//apple:utils.bzl", "bash_quote") + + +def _provisioning_cert_hex_id_command(ctx): + """Find a verified, unique hex identifer for a cert to codesign with. + + Args: + ctx: The Skylark context. + Returns: + The command invocations to find a verified hex identifer for a cert to + codesign with. + """ + if (hasattr(ctx.file, "provisioning_profile") and + not ctx.file.provisioning_profile): + fail("The provisioning_profile attribute must be set for device builds.") + + cert_name = ctx.fragments.objc.signing_certificate_name + if cert_name: + identity = cert_name + else: + # Extract the signing certificate from the provisioning profile if one was + # not explicitly provided. + extract_plist_cmd = plist_support.extract_provisioning_plist_command( + ctx, ctx.file.provisioning_profile) + identity = ("$(" + + "PLIST=$(mktemp -t cert.plist) && trap \"rm ${PLIST}\" EXIT " + + " && " + + extract_plist_cmd + " > ${PLIST} && " + + "/usr/libexec/PlistBuddy -c " + + "'Print DeveloperCertificates:0' " + + "${PLIST} | openssl x509 -inform DER -noout -fingerprint | " + + "cut -d= -f2 | sed -e s#:##g" + + ")") + + # If we're ad hoc signing or signing is mocked for tests, don't bother + # verifying the identity in the keychain. Otherwise, verify that the identity + # matches valid, unexpired entitlements in the keychain and return the first + # unique hexadecimal identifier. + if cert_name == "-" or mock_support.is_provisioning_mocked(ctx): + return "VERIFIED_ID=" + bash_quote(identity) + "\n" + + verified_id = ("VERIFIED_ID=" + + "$(" + + "security find-identity -v -p codesigning | " + + "grep -F " + bash_quote(identity) + " | " + + "xargs | " + + "cut -d' ' -f2 " + + ")\n") + # Exit and report an Xcode-visible error if no matched identifiers were found. + error_handling = ("if [ -z \"$VERIFIED_ID\" ]; then\n" + + " " + + "echo " + + bash_quote("error: Could not find a valid identity in " + + "the keychain matching " + + '"' + identity + '"' + " " + + "found in provisioning profile " + + ctx.file.provisioning_profile.path + ".") + + "\n" + + " " + + "exit 1\n" + + "fi\n") + return verified_id + error_handling + + +def _embedded_provisioning_profile_name(ctx): + """Returns the name of the embedded provisioning profile for the target. + + On macOS, the name of the provisioning profile that is placed in the bundle is + named `embedded.provisionprofile`. On all other Apple platforms, it is named + `embedded.mobileprovision`. + + Args: + ctx: The Skylark context. + Returns: + The name of the embedded provisioning profile in the bundle. + """ + if platform_support.platform_type(ctx) == apple_common.platform_type.macos: + return "embedded.provisionprofile" + return "embedded.mobileprovision" + + +def _codesign_command(ctx, + dir_to_sign, + entitlements_file, + optional=False): + """Returns a single `codesign` command invocation. + + Args: + ctx: The Skylark context. + dir_to_sign: The path inside the archive to the directory to sign. + entitlements_file: The entitlements file to pass to codesign. May be `None` + for simulator builds or non-app binaries (e.g. test bundles). + optional: If true, silently do nothing if the target directory does + not exist. This is off by default to catch errors for "mandatory" + sign paths. + Returns: + The codesign command invocation for the given directory. + """ + full_dir = "$WORK_DIR/" + dir_to_sign + cmd_prefix = "" + if optional: + cmd_prefix += "ls %s >& /dev/null && " % full_dir + + # The command returned by this function is executed as part of the final + # bundling shell script. Each directory to be signed must be prefixed by + # $WORK_DIR, which is the variable in that script that contains the path + # to the directory where the bundle is being built. + if platform_support.is_device_build(ctx): + entitlements_flag = "" + if entitlements_file: + entitlements_flag = ( + "--entitlements %s" % bash_quote(entitlements_file.path)) + + return (cmd_prefix + "/usr/bin/codesign --force " + + "--sign $VERIFIED_ID %s %s" % (entitlements_flag, full_dir)) + else: + # Use ad hoc signing for simulator builds. + full_dir = "$WORK_DIR/" + dir_to_sign + return cmd_prefix + '/usr/bin/codesign --force --sign "-" %s' % full_dir + + +def _signing_command_lines(ctx, + bundle_path_in_archive, + entitlements_file): + """Returns a multi-line string with codesign invocations for the bundle. + + Args: + ctx: The Skylark context. + bundle_path_in_archive: The path to the bundle inside the archive. + entitlements_file: The entitlements file to pass to codesign.) + Returns: + A multi-line string with codesign invocations for the bundle. + """ + + commands = [] + if platform_support.is_device_build(ctx): + commands.append(_provisioning_cert_hex_id_command(ctx)) + commands.append(_codesign_command(ctx, + bundle_path_in_archive + "/Frameworks/*", + entitlements_file, + optional=True)) + commands.append(_codesign_command(ctx, + bundle_path_in_archive, + entitlements_file)) + return "\n".join(commands) + + +# Define the loadable module that lists the exported symbols in this file. +codesigning_support = struct( + embedded_provisioning_profile_name=_embedded_provisioning_profile_name, + signing_command_lines=_signing_command_lines, +) diff --git a/apple/bundling/dSYM-Info.plist.template b/apple/bundling/dSYM-Info.plist.template new file mode 100644 index 0000000000..58a4feb7be --- /dev/null +++ b/apple/bundling/dSYM-Info.plist.template @@ -0,0 +1,20 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleIdentifier + com.apple.xcode.dsym.%bundle_name_with_extension%.dSYM + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + dSYM + CFBundleSignature + ???? + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/apple/bundling/dsym_actions.bzl b/apple/bundling/dsym_actions.bzl new file mode 100644 index 0000000000..7417625784 --- /dev/null +++ b/apple/bundling/dsym_actions.bzl @@ -0,0 +1,81 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions to manipulate dSYM bundles.""" + +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:file_actions.bzl", "file_actions") + + +def _create_symbol_bundle(ctx): + """Creates the .dSYM bundle next to the output archive. + + The generated bundle will have the same name as the bundle being built + (including its extension), but with the ".dSYM" extension appended to it. + + If the target being built does not have a binary or if the build it not + generating debug symbols (`--apple_generate_dsym` is not provided), then this + function is a no-op that returns an empty list. + + This function assumes that the target has a user-provided binary in the + `binary` attribute. It is the responsibility of the caller to check this. + + Args: + ctx: The Skylark context. + Returns: + A list of files that comprise the .dSYM bundle, which should be returned as + additional outputs from the rule. + """ + debug_outputs = ctx.attr.binary[apple_common.AppleDebugOutputs] + if not debug_outputs: + return [] + + bundle_name = bundling_support.bundle_name(ctx) + bundle_name_with_extension = bundling_support.bundle_name_with_extension(ctx) + dsym_bundle_name = bundle_name_with_extension + ".dSYM" + + outputs = [] + + # TODO(b/36174487): Iterate over .items() once the Map/dict problem is fixed. + for arch in debug_outputs.outputs_map: + arch_outputs = debug_outputs.outputs_map[arch] + dsym_binary = arch_outputs["dsym_binary"] + out_symbols = ctx.new_file("%s/Contents/Resources/DWARF/%s_%s" % ( + dsym_bundle_name, bundle_name, arch)) + outputs.append(out_symbols) + file_actions.symlink(ctx, dsym_binary, out_symbols) + + # If we found any outputs, create the Info.plist for the bundle as well; + # otherwise, we just return the empty list. The plist generated by dsymutil + # only varies based on the bundle name, so we regenerate it here rather than + # propagate the other one from the apple_binary. (See + # https://github.com/llvm-mirror/llvm/blob/master/tools/dsymutil/dsymutil.cpp) + if outputs: + out_plist = ctx.new_file("%s/Contents/Info.plist" % dsym_bundle_name) + outputs.append(out_plist) + ctx.template_action( + template=ctx.file._dsym_info_plist_template, + output=out_plist, + substitutions={ + "%bundle_name_with_extension%": bundle_name_with_extension, + }) + + return outputs + + +# Define the loadable module that lists the exported symbols in this file. +dsym_actions = struct( + create_symbol_bundle=_create_symbol_bundle, +) diff --git a/apple/bundling/entitlements.bzl b/apple/bundling/entitlements.bzl new file mode 100644 index 0000000000..883e025481 --- /dev/null +++ b/apple/bundling/entitlements.bzl @@ -0,0 +1,402 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions that manipulate entitlements and provisioning profiles.""" + +load("//apple/bundling:platform_support.bzl", + "platform_support") +load("//apple/bundling:plist_actions.bzl", "plist_actions") +load("//apple/bundling:plist_support.bzl", "plist_support") +load("//apple:utils.bzl", "apple_action") +load("//apple:utils.bzl", "bash_quote") + + +def _new_entitlements_artifact(ctx, extension): + """Returns a new file artifact for entitlements. + + This function creates a new file in an "entitlements" directory in the + target's location whose name is the target's name with the given extension. + + Args: + ctx: The Skylark context. + extension: The file extension (including the leading dot). + Returns: + The requested file object. + """ + return ctx.new_file("entitlements/%s%s" % (ctx.label.name, extension)) + + +def _extract_team_prefix_action(ctx): + """Extracts the team prefix from the target's provisioning profile. + + Args: + ctx: The Skylark context. + Returns: + The file containing the team prefix extracted from the provisioning + profile. + """ + provisioning_profile = ctx.file.provisioning_profile + extract_plist_cmd = plist_support.extract_provisioning_plist_command( + ctx, provisioning_profile) + team_prefix_file = _new_entitlements_artifact(ctx, ".team_prefix_file") + + # TODO(b/23975430): Remove the /bin/bash workaround once this bug is fixed. + apple_action( + ctx, + inputs=[provisioning_profile], + outputs=[team_prefix_file], + command=[ + "/bin/bash", "-c", + ("set -e && " + + "PLIST=$(mktemp -t teamprefix.plist) && " + + "trap \"rm ${PLIST}\" EXIT && " + + extract_plist_cmd + " > ${PLIST} && " + + "/usr/libexec/PlistBuddy -c " + + "'Print ApplicationIdentifierPrefix:0' " + + "${PLIST} > " + bash_quote(team_prefix_file.path)), + ], + mnemonic = "ExtractAppleTeamPrefix", + ) + + return team_prefix_file + + +def _extract_entitlements_action(ctx): + """Extracts entitlements from the target's provisioning profile. + + Args: + ctx: The Skylark context. + Returns: + The file containing the extracted entitlements. + """ + provisioning_profile = ctx.file.provisioning_profile + extract_plist_cmd = plist_support.extract_provisioning_plist_command( + ctx, provisioning_profile) + extracted_entitlements_file = _new_entitlements_artifact( + ctx, ".entitlements_with_variables") + + # TODO(b/23975430): Remove the /bin/bash workaround once this bug is fixed. + apple_action( + ctx, + inputs=[provisioning_profile], + outputs=[extracted_entitlements_file], + command=[ + "/bin/bash", "-c", + ("set -e && " + + "PLIST=$(mktemp -t entitlements.plist) && " + + "trap \"rm ${PLIST}\" EXIT && " + + extract_plist_cmd + " > ${PLIST} && " + + "/usr/libexec/PlistBuddy -x -c 'Print Entitlements' " + + "${PLIST} > " + bash_quote(extracted_entitlements_file.path)), + ], + mnemonic = "ExtractAppleEntitlements", + ) + + return extracted_entitlements_file + + +def _substitute_entitlements_action(ctx, + input_entitlements, + team_prefix_file, + output_entitlements): + """Creates actions to substitute values in the entitlements file. + + Args: + ctx: The Skylark context. + input_entitlements: The entitlements file with placeholders that must be + substituted. + team_prefix_file: The file containing the team prefix extracted from the + provisioning profile. + output_entitlements: The file to which the substituted entitlements should + be written. + """ + bundle_id = ctx.attr.bundle_id + + ctx.action( + inputs=[input_entitlements, team_prefix_file], + outputs=[output_entitlements], + command=( + "set -e && " + + "PREFIX=\"$(cat " + bash_quote(team_prefix_file.path) + ")\" && " + + "sed " + + "-e \"s#${PREFIX}\\.\\*#${PREFIX}." + bundle_id + "#g\" " + + "-e \"s#\\$(AppIdentifierPrefix)#${PREFIX}.#g\" " + + "-e \"s#\\$(CFBundleIdentifier)#" + bundle_id + "#g\" " + + bash_quote(input_entitlements.path) + + " > " + bash_quote(output_entitlements.path) + ), + mnemonic = "SubstituteAppleEntitlements", + ) + + +def _include_debug_entitlements(ctx): + """Returns a value indicating whether debug entitlements should be used. + + Debug entitlements are used if the _debug_entitlements attribute is present + and if the --device_debug_entitlements command-line option indicates that + they should be included. + + Args: + ctx: The Skylark context. + Returns: + True if the debug entitlements should be included, otherwise False. + """ + uses_debug_entitlements = ctx.fragments.objc.uses_device_debug_entitlements + return uses_debug_entitlements and ctx.file._debug_entitlements + + +def _register_merge_entitlements_action(ctx, + input_entitlements, + merged_entitlements): + """Merges the given entitlements files into a single file. + + Args: + ctx: The Skylark context. + input_entitlements: The entitlements files to be merged. + merged_entitlements: The File where the merged entitlements will be + written. + """ + control = struct( + plists=[f.path for f in input_entitlements], + output=merged_entitlements.path, + binary=False, + ) + control_file = ctx.new_file("%s.merge-entitlements-control" % ctx.label.name) + ctx.file_action( + output=control_file, + content=control.to_json() + ) + + plist_support.plisttool_action( + ctx, + inputs=input_entitlements, + outputs=[merged_entitlements], + control_file=control_file, + mnemonic="MergeEntitlementsFiles", + ) + + +def _entitlements_impl(ctx): + """Creates actions to create files used for code signing. + + Entitlements are generated based on a plist-format entitlements file passed + into the target's entitlements attribute, or extracted from the provisioning + profile if that attribute is not present. The team prefix is extracted from + the provisioning profile and the following substitutions are performed on the + entitlements: + + - "PREFIX.*" -> "PREFIX.BUNDLE_ID" (where BUNDLE_ID is the target's bundle + ID) + - "$(AppIdentifierPrefix)" -> "PREFIX." + - "$(CFBundleIdentifier)" -> "BUNDLE_ID" + + For a device build the entitlements are part of the code signature. + For a simulator build the entitlements are written into a Mach-O section + __TEXT,__entitlements. + + This rule generates both the entitlements file to be embedded into the code + signature, and a source file that should be compiled in to the binary to + generate the proper section in the simulator build. This file contains + preprocessor guards to ensure that its contents are only included during + simulator builds, so it is safe to add to the binary's `srcs` + unconditionally (this is necessary because the macro that invokes this rule + does not have access to enough contextual information to make that decision + on its own). + + Additionally, this rule propagates an `objc` provider. For optimized device + builds (i.e., release builds), the provider is empty. For simulator builds, + it contains additional `linkopts` that are necessary to ensure that the + generated entitlements function is linked into the appropriate Mach-O + segment. For this reason, in addition to the source file above, the target + generated by this rule must also be added as an extra `deps` of the binary + target so that the correct linker flags are included. + + Args: + ctx: The Skylark context. + Returns: + A `struct` containing the `objc` provider that propagates the additional + linker options, if necessary. + """ + is_device = platform_support.is_device_build(ctx) + if not ctx.file.provisioning_profile and is_device: + fail("The provisioning_profile attribute must be set for device builds.") + + team_prefix_file = _extract_team_prefix_action(ctx) + + # Use the entitlements from the target if given; otherwise, extract them from + # the provisioning profile. + entitlements_needing_substitution = ( + ctx.file.entitlements or _extract_entitlements_action(ctx)) + + # The ordering of this can be slightly confusing because the actions aren't + # registered in the same order that they would be executed (because + # registering actions just builds the dependency graph). If debug + # entitlements are not being included, we simply make substitutions in the + # target's entitlements and write that to the final entitlements file. If + # debug entitlements are included, then we make the substitutions in the + # target's entitlements, merge that with the debug entitlements, and the + # result is used as the final entitlements. + if _include_debug_entitlements(ctx): + substituted_entitlements = _new_entitlements_artifact(ctx, ".substituted") + + _register_merge_entitlements_action( + ctx, + input_entitlements=[ + substituted_entitlements, + ctx.file._debug_entitlements + ], + merged_entitlements=ctx.outputs.device_entitlements) + else: + substituted_entitlements = ctx.outputs.device_entitlements + + _substitute_entitlements_action(ctx, entitlements_needing_substitution, + team_prefix_file, + substituted_entitlements) + + symbol_function = "void %s(){}" % (_simulator_function(ctx.label.name)) + device_path = bash_quote(ctx.outputs.device_entitlements.path) + simulator_path = bash_quote(ctx.outputs.simulator_source.path) + ctx.action( + inputs=[ctx.outputs.device_entitlements], + outputs=[ctx.outputs.simulator_source], + # Add the empty function that we require to enforce linkage then + # add the commented out plist text + # and the the plist text as assembly bytes wrapped in + # an #ifdef that only brings them in for simulator builds. + command=( + "set -e && " + + "echo " + + "\"#include \n\n" + symbol_function + + "\n\n#if TARGET_OS_SIMULATOR\n\" >> " + + simulator_path + " && " + + " cat " + device_path + " | sed -e 's:^:// :' " + + " >> " + simulator_path + " && " + "xxd -i " + device_path + + " | sed -e '1 s/^.*$/" + + "__asm(\".section __TEXT,__entitlements\");__asm(\".byte /'" + + " -e 's/$/ \\\\/' -e '$d' | sed -e '$ s/^.*$/\");/'" + + " >> " + simulator_path + " && " + + "echo \"\n#endif // TARGET_OS_SIMULATOR\n\" >> " + simulator_path + ), + mnemonic = "GenerateSimulatorEntitlementsSource", + ) + + # Only propagate linkopts for simulator builds. We need to prevent the -u + # option from being added to release builds because it is incompatible with + # Bitcode, if users have that enabled as well. + if not is_device: + return struct(objc=apple_common.new_objc_provider( + linkopt=depset(_link_opts(ctx.label.name), order="topological"), + )) + else: + return struct(objc=apple_common.new_objc_provider()) + + +def _device_file_label(name): + """Derive the name for the `*.entitlements` file for a device build. + + Args: + name: The name of the rule that the label applies to. + Returns: + The name for the `*.entitlements` file for a device build. + """ + return name + ".entitlements" + + +def _simulator_file_label(name): + """Derive the name for the entitlements source file for a simulator build. + + Args: + name: The name of the rule that the label applies to. + Returns: + The name for the `*.entitlements.c` file for a simulator build. + """ + return _device_file_label(name) + ".c" + + +def _simulator_function(name): + """Derive the name of the function to force linkage. + + The Mach-O section that we need in our simulator binary is compiled into + a archive (.a) file. We need to force the linker to pull this section into + the actual binary. The only way to do this is to actually link in function + from the .a. We generate an empty function that we then ask the linker to + link in for us (using the link commands generated by the `link_opts` macro). + + Args: + name: The name of the rule that the label applies to. + Returns: + The name of a function to enforce linkage. + """ + return "__ENTITLEMENTS_LINKAGE__" + name + + +def _link_opts(name): + """Derive the link options required to pull in our Mach-O section. + + Returns the link options that should be passed into the linker to get it + to link in our empty function which in turn will pull in our Mach-O segment. + + Note that the symbol is prefixed with a _ because of C linkage rules. + + Args: + name: The name of the rule that the label applies to. + Returns: + The link options required to pull in our Mach-O segment appropriately + """ + return [ "-u", "_" + _simulator_function(name) ] + + +entitlements = rule( + implementation=_entitlements_impl, + attrs={ + "bundle_id": attr.string( + mandatory=True, + ), + "_debug_entitlements": attr.label( + cfg="host", + allow_files=True, + single_file=True, + default=Label("@bazel_tools//tools/objc:device_debug_entitlements.plist"), + ), + "entitlements": attr.label( + allow_files=[".entitlements", ".plist"], + single_file=True, + ), + "_plisttool": attr.label( + cfg="host", + single_file=True, + default=Label("//apple/bundling:plisttool"), + ), + # Used to pass the platform type through from the calling rule. + "platform_type": attr.string(), + "provisioning_profile": attr.label( + allow_files=[".mobileprovision"], + single_file=True, + default=Label("@bazel_tools//tools/objc:default_provisioning_profile") + ), + }, + fragments=["apple", "objc"], + outputs={ + "device_entitlements": _device_file_label("%{name}"), + "simulator_source": _simulator_file_label("%{name}"), + }, +) + + +# Define the loadable module that lists the exported macros in this file. +# Note that the entitlements rule is exported separately. +entitlements_support = struct( + device_file_label=_device_file_label, + simulator_file_label=_simulator_file_label, +) diff --git a/apple/bundling/file_actions.bzl b/apple/bundling/file_actions.bzl new file mode 100644 index 0000000000..7872526211 --- /dev/null +++ b/apple/bundling/file_actions.bzl @@ -0,0 +1,55 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions for basic file system operations.""" + + +def _symlink(ctx, source, target): + """Creates a symlink. + + This action will create the necessary directory structure for the target if + it is not present already. + + Args: + ctx: The Skylark context. + source: The source `File` of the symlink. + target: A `File` representing the target of the symlink. + """ + + # TODO(b/33386130): Create proper symlinks everywhere. + ctx.action( + inputs=[source, ctx.file._realpath], + outputs=[target], + mnemonic="Symlink", + arguments=[ + target.dirname, + source.path, + target.path, + ctx.file._realpath.path + ], + command=('mkdir -p "$1"; ' + + 'if [[ "$(uname)" == Darwin ]]; then ' + + ' ln -s "$("$4" "$2")" "$3"; ' + + "else " + + ' cp "$2" "$3"; ' + + "fi"), + progress_message="Symlinking %s to %s" % (source.path, target.path), + execution_requirements={"nosandbox": "1"}, + ) + + +# Define the loadable module that lists the exported symbols in this file. +file_actions = struct( + symlink=_symlink, +) diff --git a/apple/bundling/file_support.bzl b/apple/bundling/file_support.bzl new file mode 100644 index 0000000000..dc2d2b945f --- /dev/null +++ b/apple/bundling/file_support.bzl @@ -0,0 +1,41 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions for manipulating intermediate files.""" + +load("//apple:utils.bzl", "optionally_prefixed_path") + + +def _intermediate(ctx, pattern, prefix=None): + """Returns a new intermediate file. + + Args: + ctx: The Skylark context. + pattern: A pattern used to derive the path and name of the file. If the + placeholder `%{name}` is in the string, it will be replaced with + `ctx.label.name` (that is, the name of the current building target). + prefix: An optional prefix that, if present, will be added to the + beginning of the path, separated by the rest of the path by a slash. + Returns: + A new `File` object. + """ + name = optionally_prefixed_path( + pattern.replace("%{name}", ctx.label.name), prefix) + return ctx.new_file(name) + + +# Define the loadable module that lists the exported symbols in this file. +file_support = struct( + intermediate=_intermediate, +) diff --git a/apple/bundling/ios_rules.bzl b/apple/bundling/ios_rules.bzl new file mode 100644 index 0000000000..96c95107af --- /dev/null +++ b/apple/bundling/ios_rules.bzl @@ -0,0 +1,290 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rule implementations for creating iOS applications and bundles. + +DO NOT load this file directly; use the macro in +//apple:ios.bzl instead. Bazel rules receive their name at +*definition* time based on the name of the global to which they are assigned. +We want the user to call macros that have the same name, to get automatic +binary creation, entitlements support, and other features--which requires a +wrapping macro because rules cannot invoke other rules. +""" + +load("//apple/bundling:apple_bundling_aspect.bzl", + "apple_bundling_aspect") +load("//apple/bundling:bundler.bzl", "bundler") +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:entitlements.bzl", + "entitlements", + "entitlements_support") +load("//apple/bundling:rule_attributes.bzl", + "common_rule_attributes", + "common_rule_without_binary_attributes") +load("//apple/bundling:run_actions.bzl", "run_actions") +load("//apple/bundling:test_support.bzl", "test_support") +load("//apple:utils.bzl", "merge_dictionaries") + + +def _ios_application_impl(ctx): + """Implementation of the ios_application Skylark rule.""" + + # Add the launch storyboard if we don't already have it in the set (which may + # happen if users glob "*.storyboard", for example). + additional_resources = depset() + launch_storyboard = ctx.file.launch_storyboard + if launch_storyboard: + additional_resources = depset([launch_storyboard]) + + additional_resources += ctx.files.app_icons + ctx.files.launch_images + + # If a settings bundle was provided, pass in its bundlable files (structs + # with a File object and destination path) to the core bundler, but only + # after transforming the paths to ensure that the files are copied into a + # directory named Settings.bundle. + additional_bundlable_files = [] + settings_bundle = ctx.attr.settings_bundle + if settings_bundle: + files = settings_bundle.objc.bundle_file + additional_bundlable_files = [ + bundling_support.force_settings_bundle_prefix(f) for f in files] + + # TODO(b/32910122): Obtain framework information from extensions. + embedded_bundles = [ + bundling_support.embedded_bundle( + "PlugIns", extension.apple_bundle, verify_bundle_id=True) + for extension in ctx.attr.extensions + ] + [ + bundling_support.embedded_bundle( + "Frameworks", framework.apple_bundle, verify_bundle_id=False) + for framework in ctx.attr.frameworks + ] + + watch_app = ctx.attr.watch_application + if watch_app: + embedded_bundles.append(bundling_support.embedded_bundle( + "Watch", watch_app.apple_bundle, verify_bundle_id=True)) + + providers, additional_outputs = bundler.run( + ctx, + "IosApplicationArchive", "iOS application", + ctx.attr.bundle_id, + additional_bundlable_files=additional_bundlable_files, + additional_resources=additional_resources, + embedded_bundles=embedded_bundles, + ) + + if ctx.attr.binary: + providers["xctest_app"] = test_support.new_xctest_app_provider(ctx) + + runfiles = run_actions.start_simulator(ctx) + + # The empty ios_application provider acts as a tag to let depending + # attributes restrict the targets that can be used to just iOS applications. + return struct( + files=depset([ctx.outputs.archive]) + additional_outputs, + ios_application=struct(), + runfiles=ctx.runfiles(files=runfiles), + **providers + ) + + +# All attributes available to the _ios_application rule. (Note that this does +# not include linkopts, which is consumed entirely by the wrapping macro.) +_IOS_APPLICATION_ATTRIBUTES = merge_dictionaries(common_rule_attributes(), { + "app_icons": attr.label_list(), + "entitlements": attr.label( + allow_files=[".entitlements"], + single_file=True, + ), + "extensions": attr.label_list( + providers=[["apple_bundle", "ios_extension"]], + ), + "families": attr.string_list( + mandatory=True, + ), + "frameworks": attr.label_list( + allow_rules=["ios_framework"], + ), + "launch_images": attr.label_list(), + "launch_storyboard": attr.label( + allow_files=[".storyboard", ".xib"], + single_file=True, + ), + "product_type": attr.string(), + "settings_bundle": attr.label(providers=[["objc"]]), + "watch_application": attr.label( + providers=[["apple_bundle", "watchos_application"]], + ), + "_allowed_families": attr.string_list(default=["iphone", "ipad"]), + # The extension of the bundle being generated by the rule. + "_bundle_extension": attr.string(default=".app"), + # iOS .app bundles should include a PkgInfo file. + "_needs_pkginfo": attr.bool(default=True), + # A format string used to compose the path to the bundle inside the + # packaged archive. The placeholder "%s" is replaced with the name of the + # bundle (with its extension). + "_path_in_archive_format": attr.string(default="Payload/%s"), + # The platform type that should be passed to tools for targets of this + # type. + "_platform_type": attr.string(default=str(apple_common.platform_type.ios)), +}) + + +ios_application = rule( + _ios_application_impl, + attrs = _IOS_APPLICATION_ATTRIBUTES, + executable = True, + fragments = ["apple", "objc"], + outputs = { + "archive": "%{name}.ipa", + }, +) + + +def _ios_extension_impl(ctx): + """Implementation of the ios_extension Skylark rule.""" + additional_resources = depset(ctx.files.app_icons + ctx.files.asset_catalogs) + + providers, additional_outputs = bundler.run( + ctx, + "IosExtensionArchive", "iOS extension", + ctx.attr.bundle_id, + additional_resources=additional_resources, + ) + + # The empty ios_extension provider acts as a tag to let depending attributes + # restrict the targets that can be used to just iOS extensions. + return struct( + files=depset([ctx.outputs.archive]) + additional_outputs, + ios_extension = struct(), + **providers + ) + + +# All attributes available to the _ios_extension rule. (Note that this does +# not include linkopts, which is consumed entirely by the wrapping macro.) +_IOS_EXTENSION_ATTRIBUTES = merge_dictionaries(common_rule_attributes(), { + "app_icons": attr.label_list(), + "asset_catalogs": attr.label_list( + allow_files=True, + ), + "entitlements": attr.label( + allow_files=[".entitlements"], + single_file=True, + ), + "families": attr.string_list( + mandatory=True, + ), + "frameworks": attr.label_list( + allow_rules=["ios_framework"], + ), + "product_type": attr.string(), + "_allowed_families": attr.string_list(default=["iphone", "ipad"]), + # The extension of the bundle being generated by the rule. + "_bundle_extension": attr.string(default=".appex"), + # iOS extension bundles should not include a PkgInfo file. + "_needs_pkginfo": attr.bool(default=False), + # A format string used to compose the path to the bundle inside the + # packaged archive. The placeholder "%s" is replaced with the name of the + # bundle (with its extension). + "_path_in_archive_format": attr.string(default="%s"), + # The platform type that should be passed to tools for targets of this + # type. + "_platform_type": attr.string(default=str(apple_common.platform_type.ios)), + "_propagates_frameworks": attr.bool(default=True), +}) + + +ios_extension = rule( + _ios_extension_impl, + attrs = _IOS_EXTENSION_ATTRIBUTES, + fragments = ["apple", "objc"], + outputs = { + "archive": "%{name}.zip", + }, +) + + +def _ios_framework_impl(ctx): + """Implementation of the ios_framework Skylark rule.""" + bundlable_binary = struct(file=ctx.file.binary, + bundle_path=bundling_support.bundle_name(ctx)) + prefixed_hdr_files = [] + for hdr_provider in ctx.attr.hdrs: + for hdr_file in hdr_provider.files: + prefixed_hdr_files.append(bundling_support.header_prefix(hdr_file)) + + providers, additional_outputs = bundler.run( + ctx, + "IosFrameworkArchive", "iOS framework", + ctx.attr.bundle_id, + additional_bundlable_files=prefixed_hdr_files, + framework_files=prefixed_hdr_files + [bundlable_binary], + is_dynamic_framework=True, + ) + + return struct( + files=depset([ctx.outputs.archive]) + additional_outputs, + **providers + ) + + +# All attributes available to the _ios_framework rule. +_IOS_FRAMEWORK_ATTRIBUTES = merge_dictionaries(common_rule_without_binary_attributes(), { + "app_icons": attr.label_list(), + "binary": attr.label( + # TODO(b/36513471): Restrict to apple dylib provider. + allow_rules=["apple_binary"], + aspects=[apple_bundling_aspect], + mandatory=True, + single_file=True, + ), + "hdrs": attr.label_list( + allow_files=[".h"], + ), + "families": attr.string_list( + mandatory=True, + ), + "linkopts": attr.string_list(), + "_allowed_families": attr.string_list(default=["iphone", "ipad"]), + # The extension of the bundle being generated by the rule. + "_bundle_extension": attr.string(default=".framework"), + # iOS extension bundles should not include a PkgInfo file. + "_needs_pkginfo": attr.bool(default=False), + # A format string used to compose the path to the bundle inside the + # packaged archive. The placeholder "%s" is replaced with the name of the + # bundle (with its extension). + "_path_in_archive_format": attr.string(default="%s"), + # The platform type that should be passed to tools for targets of this + # type. + "_platform_type": attr.string(default=str(apple_common.platform_type.ios)), + # Frameworks don't nest other frameworks; such dependencies should be + # propagated to the same place as the parent target's frameworks. + "_propagates_frameworks": attr.bool(default=True), + # Frameworks do not require code signing by themselves, and are signed only + # when the containing app or extension is signed. + "_skip_signing": attr.bool(default=True), +}) + + +ios_framework = rule( + _ios_framework_impl, + attrs = _IOS_FRAMEWORK_ATTRIBUTES, + fragments = ["apple", "objc"], + outputs = { + "archive": "%{name}.zip", + }, +) diff --git a/apple/bundling/mock_support.bzl b/apple/bundling/mock_support.bzl new file mode 100644 index 0000000000..967eef65a4 --- /dev/null +++ b/apple/bundling/mock_support.bzl @@ -0,0 +1,44 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions to detect mocking during integration tests.""" + + +def _is_provisioning_mocked(ctx): + """Returns a value indicating if provisioning operations should be mocked. + + Provisioning operations will be mocked if the flag + `--define=bazel_apple_rules.mock_provisioning=true` is passed during the + build. + + If provisioning is mocked, then provisioning profile extractions (team prefix + ID and entitlements) are not preceded by a call to the `security cms -D` + command; instead, the provisioning profile is used as-is. This allows a plain + (unsigned) XML plist with the expected contents be used during integration + test builds, rather than requiring a proper one obtained from Apple (which + would have to be associated with a real developer account). + + Args: + ctx: The Skylark context. + Returns: + The list of device families that apply to the target being built. + """ + return ctx.var.get( + "bazel_rules_apple.mock_provisioning", "").lower() == "true" + + +# Define the loadable module that lists the exported symbols in this file. +mock_support = struct( + is_provisioning_mocked=_is_provisioning_mocked, +) diff --git a/apple/bundling/modulemap_actions.bzl b/apple/bundling/modulemap_actions.bzl new file mode 100644 index 0000000000..e6822e4b2b --- /dev/null +++ b/apple/bundling/modulemap_actions.bzl @@ -0,0 +1,101 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions that operate on modulemap files. + +See documentation: https://clang.llvm.org/docs/Modules.html#module-map-language +""" + +load("//apple/bundling:file_support.bzl", "file_support") +load("//apple/bundling:provider_support.bzl", + "provider_support") + + +def _link_declarations(ctx): + """Generates required link declarations for the current module. + + Args: + ctx: The Skylark context. + Returns: + A list of library and framework link declarations against which a program + should be linked if the module is imported. + """ + # TODO(b/36513020): Generalize this by having the caller pass in dylibs and + # frameworks rather than deriving them from the ctx. + sdk_dylibs = depset() + sdk_frameworks = depset() + objc_providers = provider_support.binary_or_deps_providers(ctx, "objc") + for objc in objc_providers: + sdk_frameworks += objc.sdk_framework + sdk_dylibs += objc.sdk_dylib + + link_declarations = [] + for dylib in sdk_dylibs: + # sdk_dylibs are passed in with a preceding "lib" e.g., libc++ - which + # must be stripped for the link declarations in the module map - e.g., link + # "c++". + if not dylib.startswith("lib"): + fail("linked sdk_dylib %s name must start with lib" % dylib) + dylib_name = dylib[3:] + link_declarations.append('link "%s"' % dylib_name) + for framework_name in sdk_frameworks: + link_declarations.append('link framework "%s"' % framework_name) + return sorted(link_declarations) + + +def _framework_module_declaration(framework_name, members): + """Generates a framework module declaration. + + Args: + framework_name: The name of the framework module. + members: Members including the headers that contribute to that module, its + submodules, and other aspects of the module. + Returns: + A module declaration string. + """ + module_declaration = ["framework module %s {" % framework_name] + module_declaration += [" " + member for member in members] + module_declaration += ["}"] + return "\n".join(module_declaration) + + +def _create_modulemap(ctx, framework_name, umbrella_header_filename): + """Registers an action that creates a modulemap for the bundle. + + Args: + ctx: The Skylark context. + framework_name: The name of the framework. + umbrella_header_filename: The name of the umbrella header for the framework. + Returns: + A modulemap `File` for the current module. + """ + output_modulemap = file_support.intermediate(ctx, "%{name}-module.modulemap") + + module_members = [ + 'umbrella header "%s"' % umbrella_header_filename, + "export *", + "module * { export * }" + ] + module_members += _link_declarations(ctx) + + module_declaration = _framework_module_declaration(framework_name, + module_members) + ctx.file_action(output=output_modulemap, content=module_declaration) + return output_modulemap + + +# Define the loadable module that lists the exported symbols in this file. +modulemap_actions = struct( + create_modulemap=_create_modulemap, +) diff --git a/apple/bundling/platform_support.bzl b/apple/bundling/platform_support.bzl new file mode 100644 index 0000000000..bf8c5853ff --- /dev/null +++ b/apple/bundling/platform_support.bzl @@ -0,0 +1,159 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions for working with Apple platforms and device families.""" + +load("//apple:utils.bzl", "apple_action") + + +# Maps the strings passed in to the "families" attribute to the numerical +# representation in the UIDeviceFamily plist entry. +_DEVICE_FAMILY_VALUES = { + "iphone": 1, + "ipad": 2, + "tv": 3, + "watch": 4, + # We want _family_plist_number to return None for the valid "mac" family + # since macOS doesn't use the UIDeviceFamily Info.plist key, but we still + # want to catch invalid families with a KeyError. + "mac": None, +} + + +def _families(ctx): + """Returns the device families that apply to the target being built. + + Some platforms, such as iOS, support multiple device families (iPhone and + iPad) and provide a `families` attribute that lets the user specify which + to use. Other platforms, like tvOS, only support one family, so they do not + provide the public attribute and instead we implicitly get the supported + families from the private attribute instead. + + Args: + ctx: The Skylark context. + Returns: + The list of device families that apply to the target being built. + """ + if hasattr(ctx.attr, "families"): + return ctx.attr.families + return ctx.attr._allowed_families + + +def _family_plist_number(family_name): + """Returns the `UIDeviceFamily` integer for a device family. + + This function returns None for valid device families that do not use the + `UIDeviceFamily` Info.plist key (currently, only `mac`). + + Args: + family_name: The device family name, as given in the `families` attribute + of an Apple bundle target. + Returns: + The integer to use in the `UIDeviceFamily` key of an Info.plist file, or + None if the key should not be added to the Info.plist. + """ + return _DEVICE_FAMILY_VALUES[family_name] + + +def _is_device_build(ctx): + """Returns True if the target is being built for a device. + + Args: + ctx: The Skylark context. + Returns: + True if this is a device build, or False if it is a simulator build. + """ + platform, _ = _platform_and_sdk_version(ctx) + return platform.is_device + + +def _minimum_os(ctx): + """Returns the minimum OS version required for the current target. + + TODO(b/31753863): Grab this from the target once we support setting the + minimum OS there. + + Args: + ctx: The Skylark context. + Returns: + A `DottedVersion` object representing the minimum OS version. + """ + apple = ctx.fragments.apple + return apple.minimum_os_for_platform_type(_platform_type(ctx)) + + +def _platform_type(ctx): + """Returns the platform type for the current target. + + Args: + ctx: The Skylark context. + Returns: + The `PlatformType` for the current target, after being converted from its + string attribute form. + """ + if hasattr(ctx.attr, "platform_type"): + platform_type_string = ctx.attr.platform_type + else: + platform_type_string = ctx.attr._platform_type + return getattr(apple_common.platform_type, platform_type_string) + + +def _platform_and_sdk_version(ctx): + """Returns the platform and SDK version for the current target. + + Args: + ctx: The Skylark context. + Returns: + A tuple containing the Platform object for the target and the SDK version + to build against for that platform. + """ + apple = ctx.fragments.apple + platform = apple.multi_arch_platform(_platform_type(ctx)) + sdk_version = apple.sdk_version_for_platform(platform) + + return platform, sdk_version + + +def _xcode_env_action(ctx, **kwargs): + """Executes a Darwin-only action with the necessary platform environment. + + This rule is intended to be used by actions that invoke scripts like + actoolwrapper and ibtoolwrapper that need to pass the Xcode and target + platform versions into the environment but don't need to be wrapped by + xcrunwrapper because they already invoke it internally. + + Rules using this action must require the "apple" configuration fragment. + + Args: + ctx: The Skylark context. + **kwargs: Arguments to be passed into apple_action. + """ + platform, _ = _platform_and_sdk_version(ctx) + apple = ctx.fragments.apple + action_env = apple.target_apple_env(platform) + apple.apple_host_system_env() + kwargs["env"] = kwargs.get("env", {}) + action_env + + apple_action(ctx, **kwargs) + + +# Define the loadable module that lists the exported symbols in this file. +platform_support = struct( + families=_families, + family_plist_number=_family_plist_number, + is_device_build=_is_device_build, + minimum_os=_minimum_os, + platform_and_sdk_version=_platform_and_sdk_version, + platform_type=_platform_type, + xcode_env_action=_xcode_env_action, +) diff --git a/apple/bundling/plist_actions.bzl b/apple/bundling/plist_actions.bzl new file mode 100644 index 0000000000..512558a326 --- /dev/null +++ b/apple/bundling/plist_actions.bzl @@ -0,0 +1,274 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions that operate on plist files.""" + +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:file_support.bzl", "file_support") +load("//apple/bundling:plist_support.bzl", "plist_support") +load("//apple/bundling:platform_support.bzl", + "platform_support") +load("//apple/bundling:product_support.bzl", + "product_support") +load("//apple:utils.bzl", "apple_action") +load("//apple:utils.bzl", "remove_extension") + + +# Command string for "sed" that tries to extract the application version number +# from a larger string provided by the --embed_label flag. For example, from +# "foo_1.2.3_RC00" this would extract "1.2.3". This regex looks for versions of +# the format "x.y" or "x.y.z", which may be preceded and/or followed by other +# text, such as a project name or release candidate number. This command also +# preserves double quotes around the string, if any. +# +# This sed command is not terribly readable because sed requires parens and +# braces to be escaped and it does not support '?' or '+'. So, this command +# corresponds to the following regular expression: +# +# ("){0,1} # Group 1: optional starting quotes +# (.*_){0,1} # Group 2: anything (optional) before an underscore +# ([0-9][0-9]*(\.[0-9][0-9]*){1,2}) # Group 3: capture anything that looks +# # like a version number of the form x.y +# # or x.y.z (group 4 is for nesting only) +# (_[^"]*){0,1} # Group 5: anything (optional) after an underscore +# ("){0,1} # Group 6: optional closing quotes +# +# Then, the replacement extracts "\1\3\6" -- in other words, the version number +# component, surrounded by quotes if they were present in the original string. +_EXTRACT_VERSION_SED_COMMAND = ( + r's#\("\)\{0,1\}\(.*_\)\{0,1\}\([0-9][0-9]*\(\.[0-9][0-9]*\)' + + r'\{1,2\}\)\(_[^"]*\)\{0,1\}\("\)\{0,1\}#\1\3\6#') + + +def _environment_plist_action(ctx): + """Creates an action that extracts the Xcode environment plist. + + Args: + ctx: The Skylark context. + Returns: + The plist file that contains the extracted environment. + """ + platform, sdk_version = platform_support.platform_and_sdk_version(ctx) + platform_with_version = platform.name_in_plist.lower() + str(sdk_version) + + environment_plist = ctx.new_file(ctx.label.name + "_environment.plist") + platform_support.xcode_env_action( + ctx, + outputs=[environment_plist], + executable=ctx.executable._environment_plist, + arguments=[ + "--platform", + platform_with_version, + "--output", + environment_plist.path, + ], + ) + + return environment_plist + + +def _version_plist_action(ctx): + """Creates an action that extracts a version number from the build info. + + The --embed_label flag can be used during the build to embed a string that + will be inspected for something that looks like a version number (for + example, "MyApp_1.2.3_prod"). If found, that string ("1.2.3") will be used + as the CFBundleVersion and CFBundleShortVersionString for the bundle. + + If no version number was found in the label (or if the flag was not + provided), the returned plist will be empty so that merging it becomes a + no-op. + + Args: + ctx: The Skylark context. + Returns: + The plist file that contains the extracted version information. + """ + version_plist = ctx.new_file(ctx.label.name + "_version.plist") + plist_path = version_plist.path + + info_path = ctx.info_file.path + + ctx.action( + inputs=[ctx.info_file], + outputs=[version_plist], + command=( + "set -e && " + + "VERSION=\"$(grep \"^BUILD_EMBED_LABEL\" " + info_path + " | " + + "cut -d\" \" -f2- | " + + "sed -e '" + _EXTRACT_VERSION_SED_COMMAND + "' | " + + "sed -e \"s#\\\"#\\\\\\\"#g\")\" && " + + "cat >" + plist_path + " <\n" + + "\n" + + "\n" + + "\n" + + "EOF\n" + + "if [[ -n \"$VERSION\" ]]; then\n" + + " for KEY in CFBundleVersion CFBundleShortVersionString; do\n" + + " echo \" $KEY\" >> " + plist_path + "\n" + + " echo \" $VERSION\" >> " + plist_path + "\n" + + " done\n" + + "fi\n" + + "cat >>" + plist_path + " <\n" + + "\n" + + "EOF\n" + ), + mnemonic="VersionPlist", + ) + + return version_plist + + +def _merge_infoplists(ctx, + path_prefix, + input_plists, + bundle_id=None, + executable_bundle=False, + child_plists=[]): + """Creates an action that merges Info.plists and converts them to binary. + + This action merges multiple plists by shelling out to plisttool, then + compiles the final result into a single binary plist file. + + This action also generates a PkgInfo file for the bundle as a side effect + of processing the appropriate keys in the plist, if the `_needs_pkginfo` + attribute on the target is True. + + Args: + ctx: The Skylark context. + path_prefix: A path prefix to apply in front of any intermediate files. + input_plists: The plist files to merge. + bundle_id: The bundle identifier to set in the output plist. + executable_bundle: If True, this action is intended for an executable + bundle's Info.plist, which means the development environment and + platform info should be added to the plist, and a PkgInfo should + (optionally) be created. + child_plists: A list of plists from child targets (such as extensions + or Watch apps) whose bundle IDs and version strings should be + validated against the compiled plist for consistency. + Returns: + A struct with two fields: `output_plist`, a File object containing the + merged binary plist, and `pkginfo`, a File object containing the PkgInfo + file (or None, if no file was generated). + """ + output_plist = file_support.intermediate( + ctx, "%{name}-Info-binary.plist", path_prefix) + + if executable_bundle and ctx.attr._needs_pkginfo: + pkginfo = file_support.intermediate(ctx, "%{name}-PkgInfo", path_prefix) + else: + pkginfo = None + + forced_plists = [] + additional_plisttool_inputs = [] + + if hasattr(ctx.file, "launch_storyboard") and ctx.file.launch_storyboard: + launch_storyboard = ctx.file.launch_storyboard + short_name = remove_extension(launch_storyboard.basename) + forced_plists.append(struct(UILaunchStoryboardName=short_name)) + + info_plist_options = { + "bundle_name": bundling_support.bundle_name_with_extension(ctx), + "pkginfo": pkginfo.path if pkginfo else None, + } + + if executable_bundle and bundle_id: + info_plist_options["bundle_id"] = bundle_id + + # Resource bundles don't need the Xcode environment plist entries; + # application and extension bundles do. + if executable_bundle: + info_plist_options["executable"] = bundling_support.bundle_name(ctx) + + platform, sdk_version = platform_support.platform_and_sdk_version(ctx) + platform_with_version = platform.name_in_plist.lower() + str(sdk_version) + + min_os = platform_support.minimum_os(ctx) + + environment_plist = _environment_plist_action(ctx) + version_plist = _version_plist_action(ctx) + additional_plisttool_inputs = [environment_plist, version_plist] + + additional_infoplist_values = {} + + # Convert the device family names to integers used in the plist; the + # family_plist_number function handles the special case for macOS, which + # does not use UIDeviceFamily. + families = [] + for f in platform_support.families(ctx): + number = platform_support.family_plist_number(f) + if number: + families.append(number) + if families: + additional_infoplist_values["UIDeviceFamily"] = families + + # Collect any values for special product types that we have to manually put + # in (duplicating what Xcode apparently does under the hood). + product_info = product_support.product_type_info_for_target(ctx) + if product_info and product_info.additional_infoplist_values: + additional_infoplist_values = product_info.additional_infoplist_values + + forced_plists += [ + environment_plist.path, + version_plist.path, + struct( + CFBundleSupportedPlatforms=[platform.name_in_plist], + DTPlatformName=platform.name_in_plist.lower(), + DTSDKName=platform_with_version, + MinimumOSVersion=str(min_os), + **additional_infoplist_values + ), + ] + + child_plists_for_control = struct( + **{str(p.owner): p.path for p in child_plists}) + info_plist_options["child_plists"] = child_plists_for_control + + control = struct( + plists=[p.path for p in input_plists], + forced_plists=forced_plists, + output=output_plist.path, + binary=True, + info_plist_options=struct(**info_plist_options), + ) + control_file = file_support.intermediate( + ctx, "%{name}.plisttool-control", path_prefix) + ctx.file_action( + output=control_file, + content=control.to_json() + ) + + outputs = [output_plist] + if pkginfo: + outputs.append(pkginfo) + + plist_support.plisttool_action( + ctx, + inputs=input_plists + child_plists + additional_plisttool_inputs, + outputs=outputs, + control_file=control_file, + mnemonic="CompileInfoPlist", + ) + + return struct(output_plist=output_plist, pkginfo=pkginfo) + + +# Define the loadable module that lists the exported symbols in this file. +plist_actions = struct( + merge_infoplists=_merge_infoplists, +) diff --git a/apple/bundling/plist_support.bzl b/apple/bundling/plist_support.bzl new file mode 100644 index 0000000000..85bc96b60a --- /dev/null +++ b/apple/bundling/plist_support.bzl @@ -0,0 +1,78 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions for plist-based operations.""" + +load("//apple:utils.bzl", "apple_action") +load("//apple/bundling:mock_support.bzl", "mock_support") +load("//apple:utils.bzl", "bash_quote") + + +def _extract_provisioning_plist_command(ctx, provisioning_profile): + """Returns the shell command to extract a plist from a provisioning profile. + + Args: + ctx: The Skylark context. + provisioning_profile: The `File` representing the provisioning profile. + Returns: + The shell command used to extract the plist. + """ + if mock_support.is_provisioning_mocked(ctx): + # If provisioning is mocked, treat the provisioning profile as a plain XML + # plist without a signature. + return "cat " + bash_quote(provisioning_profile.path) + else: + return "security cms -D -i " + bash_quote(provisioning_profile.path) + + +def _plisttool_action(ctx, inputs, outputs, control_file, mnemonic=None): + """Registers an action that invokes `plisttool`. + + This function is a low-level helper that simply invokes `plisttool` with the + given arguments. It is intended to be called by other functions that register + actions for more specific resources, like Info.plist files or entitlements + (which is why it is in a `plist_support.bzl` rather than + `plist_actions.bzl`). + + Args: + ctx: The Skylark context. + inputs: Any `File`s that should be treated as inputs to the underlying + action. + outputs: Any `File`s that should be treated as outputs of the underlying + action. + control_file: The `File` containing the control struct to be passed to + plisttool. + mnemonic: The mnemonic to display when the action executes. Defaults to + None. + """ + plisttool = ctx.file._plisttool + + # TODO(b/23975430): Remove the /bin/bash workaround once this bug is fixed. + apple_action( + ctx, + inputs=inputs + [control_file, plisttool], + outputs=outputs, + command=[ + "/bin/bash", "-c", + "python2.7 %s %s" % (plisttool.path, control_file.path), + ], + mnemonic=mnemonic, + ) + + +# Define the loadable module that lists the exported symbols in this file. +plist_support = struct( + extract_provisioning_plist_command=_extract_provisioning_plist_command, + plisttool_action=_plisttool_action, +) diff --git a/apple/bundling/plisttool.py b/apple/bundling/plisttool.py new file mode 100644 index 0000000000..6ebb7f2194 --- /dev/null +++ b/apple/bundling/plisttool.py @@ -0,0 +1,451 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Plist manipulation for Apple packaging rules. + +The "defaults" tool provided with OS X is somewhat satisfactory for reading and +writing single values in a plist, but merging whole plists with conflict +detection is not as easy. + +This script takes a single argument that points to a file containing the JSON +representation of a "control" structure (similar to the PlMerge tool, which +takes a binary protocol buffer). This control structure is a dictionary with +the following keys: + + plists: A list of plists that will be merged. The items in this list may be + strings (which are interpreted as paths), readable file-like objects + containing XML-formatted plist data (for testing), or dictionaries that + are treated as inlined plists. Key-value pairs within the plists in this + list must not conflict (i.e., the same key must not have different values + in different plists) or the tool will raise an error. + forced_plists: A list of plists that will be merged after those in "plists". + Unlike those, collisions between key-value pairs in these plists do not + raise an error; they replace any values from the originals instead. If + multiple plists have the same key, the last one in this list is the one + that will be kept. + output: A string indicating the path to where the merged plist will be + written, or a writable file-like object (for testing). + binary: If true, the output plist file will be written in binary format; + otherwise, it will be written in XML format. This property is ignored if + |output| is not a path. + info_plist_options: A dictionary containing options specific to Info.plist + files. Omit this key if you are merging or converting general plists + (such as entitlements or other files). See below for more details. + +The info_plist_options dictionary can contain the following keys: + + executable: The name of the executable that will be written into the + CFBundleExecutable key of the final plist, and will also be used in + ${EXECUTABLE_NAME} and ${PRODUCT_NAME} substitutions. + bundle_name: The bundle name (that is, the executable name and extension) + that is used in the ${BUNDLE_NAME} substitution. + bundle_id: The bundle identifier that will be written into the + CFBundleIdentifier key of the final plist and will be used in the + ${PRODUCT_BUNDLE_IDENTIFIER} substitution. + pkginfo: If present, a string that denotes the path to a PkgInfo file that + should be created from the CFBundlePackageType and CFBundleSignature keys + in the final merged plist. (For testing purposes, this may also be a + writable file-like object.) + child_plists: If present, a dictionary containing plists that will be + compared against the final compiled plist for consistency. The keys of + the dictionary are the labels of the targets to which the associated + plists belong. See below for the details of how these are validated. + +If info_plist_options is present, validation will be performed on the output +file after merging is complete. If any of the following conditions are not +satisfied, an error will be raised: + + * The CFBundleIdentifier must be present and be formatted as a valid bundle + identifier. + * The CFBundleIdentifier and CFBundleShortVersionString values of the + output file will be compared to the child plists for consistency. Child + plists are expected to have the same bundle version string as the parent + and should have bundle IDs that are prefixed by the bundle ID of the + parent. +""" + +from collections import OrderedDict +import json +import plistlib +import re +import subprocess +import sys + +MISMATCHED_BUNDLE_ID_MSG = ('The CFBundleIdentifier of the merged Info.plists ' + '"%s" must be equal to the bundle_id argument ' + '"%s".') + +CHILD_BUNDLE_ID_MISMATCH_MSG = ('The CFBundleIdentifier of the child target ' + '"%s" should have "%s" as its prefix, but ' + 'found "%s".') + +CHILD_BUNDLE_VERSION_MISMATCH_MSG = ('The CFBundleShortVersionString of the ' + 'child target "%s" should be the same as ' + 'its parent\'s version string "%s", but ' + 'found "%s".') + + +class PlistConflictError(ValueError): + """Raised when conflicting values are found for a key. + + This error is raised when two plists being merged have different values for + the same key. The "key" attribute has the name of the key; the "value1" and + "value2" attributes have the values that were encountered. + """ + + def __init__(self, key, value1, value2): + """Initializes an error with the given key and values. + + Args: + key: The key that had conflicting values. + value1: One of the conflicting values. + value2: One of the conflicting values. + """ + self.key = key + self.value1 = value1 + self.value2 = value2 + ValueError.__init__(self, ( + 'Found key %r in two plists with different values: %r != %r') % ( + key, value1, value2)) + + +class PlistTool(object): + """Implements the core functionality of the plist tool.""" + + def __init__(self, control): + """Initializes PlistTool with the given control options. + + Args: + control: The dictionary of options used to control the tool. Please see + the moduledoc for a description of the format of this dictionary. + Raises: + ValueError: If the bundle_id parameter is missing. + """ + self._control = control + + # The dictionary of substitutions to apply, where the key is the name to be + # replaced when enclosed by ${...} or $(...) in a plist value, and the + # value is the string to substitute. + self._substitutions = {} + + info_plist_options = self._control.get('info_plist_options') + if info_plist_options: + executable = info_plist_options.get('executable') + if executable: + self._substitutions['EXECUTABLE_NAME'] = executable + self._substitutions['PRODUCT_NAME'] = executable + + bundle_name = info_plist_options.get('bundle_name') + if bundle_name: + self._substitutions['BUNDLE_NAME'] = bundle_name + + bundle_id = info_plist_options.get('bundle_id') + if bundle_id: + self._substitutions['PRODUCT_BUNDLE_IDENTIFIER'] = bundle_id + + def run(self): + """Performs the operations requested by the control struct.""" + if not self._control.get('plists'): + raise ValueError('No input plists specified.') + + if not self._control.get('output'): + raise ValueError('No output file specified.') + + out_plist = {} + + for p in self._control['plists']: + plist = self._get_plist_dict(p) + self.merge_dictionaries(plist, out_plist) + + forced_plists = self._control.get('forced_plists', []) + for p in forced_plists: + plist = self._get_plist_dict(p) + self.merge_dictionaries(plist, out_plist, override_collisions=True) + + info_plist_options = self._control.get('info_plist_options') + if info_plist_options: + self._perform_info_plist_operations(out_plist, info_plist_options) + + self._write_plist(out_plist) + + def merge_dictionaries(self, src, dest, override_collisions=False): + """Merge the top-level keys from src into dest. + + This method is publicly visible for testing. + + Args: + src: The dictionary whose values will be merged into dest. + dest: The dictionary into which the values will be merged. + override_collisions: If True, collisions will be resolved by replacing + the previous value with the new value. If False, an error will be + raised if old and new values do not match. + Raises: + PlistConflictError: If the two dictionaries had different values for the + same key. + """ + for key in src: + src_value = self._apply_substitutions(src[key]) + + if key in dest: + dest_value = dest[key] + + if not override_collisions and src_value != dest_value: + raise PlistConflictError(key, src_value, dest_value) + + dest[key] = src_value + + def _get_plist_dict(self, p): + """Returns a plist dictionary based on the given object. + + This function handles the various input formats for plists in the control + struct that are supported by this tool. Dictionary objects are returned + verbatim; strings are treated as paths to plist files, and anything else + is assumed to be a readable file-like object whose contents are plist data. + + Args: + p: The object to interpret as a plist. + Returns: + A dictionary containing the values from the plist. + """ + if isinstance(p, dict): + return p + + if isinstance(p, basestring): + with open(p) as plist_file: + return OrderedDict(self._read_plist(plist_file)) + + return OrderedDict(self._read_plist(p)) + + def _read_plist(self, plist_file): + """Reads a plist file and returns its contents as a dictionary. + + This method wraps the readPlist method in plistlib by checking the format + of the plist before reading and using plutil to convert it into XML format + first, to support plain text and binary formats as well. + + Args: + plist_file: The file-like object containing the plist data. + Returns: + The contents of the plist file as a dictionary. + """ + plist_contents = plist_file.read() + + # Binary plists are easy to identify because they start with 'bplist'. For + # plain text plists, it may be possible to have leading whitespace, but + # well-formed XML should *not* have any whitespace before the XML + # declaration, so we can check that the plist is not XML and let plutil + # handle them the same way. + if not plist_contents.startswith('\n' + '\n' + '\n' + '\n' + + content + '\n' + + '\n' + '\n') + return StringIO.StringIO(xml) + + +def _plisttool_result(control): + """Helper function that runs PlistTool with the given control struct. + + This function inserts a StringIO object as the control's "output" key and + returns the dictionary containing the result of the tool after parsing it + from that StringIO. + + Args: + control: The control struct to pass to PlistTool. See the module doc for + the plisttool module for a description of this format. + Returns: + The dictionary containing the result of the tool after parsing it from + the in-memory string file. + """ + output = StringIO.StringIO() + control['output'] = output + + tool = plisttool.PlistTool(control) + tool.run() + + return plistlib.readPlistFromString(output.getvalue()) + + +class PlistToolTest(unittest.TestCase): + + def _assert_plisttool_result(self, control, expected): + """Asserts that PlistTool's result equals the expected dictionary. + + Args: + control: The control struct to pass to PlistTool. See the module doc for + the plisttool module for a description of this format. + expected: The dictionary that represents the expected result from running + PlistTool. + """ + outdict = _plisttool_result(control) + self.assertEqual(expected, outdict) + + def _assert_pkginfo(self, plist, expected): + """Asserts that PlistTool generates the expected PkgInfo file contents. + + Args: + plist: The plist file from which to obtain the PkgInfo values. + expected: The expected 8-byte string written to the PkgInfo file. + """ + pkginfo = StringIO.StringIO() + control = { + 'plists': [plist], + 'output': StringIO.StringIO(), + 'info_plist_options': {'pkginfo': pkginfo}, + } + tool = plisttool.PlistTool(control) + tool.run() + self.assertEqual(expected, pkginfo.getvalue()) + + def test_merge_of_one_file(self): + plist1 = _xml_plist('Fooabc') + self._assert_plisttool_result({'plists': [plist1]}, {'Foo': 'abc'}) + + def test_merge_of_one_dict(self): + plist1 = {'Foo': 'abc'} + self._assert_plisttool_result({'plists': [plist1]}, {'Foo': 'abc'}) + + def test_merge_of_one_empty_file(self): + plist1 = _xml_plist('') + self._assert_plisttool_result({'plists': [plist1]}, {}) + + def test_merge_of_one_empty_dict(self): + plist1 = {} + self._assert_plisttool_result({'plists': [plist1]}, {}) + + def test_merge_of_two_files(self): + plist1 = _xml_plist('Fooabc') + plist2 = _xml_plist('Bardef') + self._assert_plisttool_result({'plists': [plist1, plist2]}, { + 'Foo': 'abc', + 'Bar': 'def', + }) + + def test_merge_of_file_and_dict(self): + plist1 = _xml_plist('Fooabc') + plist2 = {'Bar': 'def'} + self._assert_plisttool_result({'plists': [plist1, plist2]}, { + 'Foo': 'abc', + 'Bar': 'def', + }) + + def test_merge_of_two_dicts(self): + plist1 = {'Foo': 'abc'} + plist2 = {'Bar': 'def'} + self._assert_plisttool_result({'plists': [plist1, plist2]}, { + 'Foo': 'abc', + 'Bar': 'def', + }) + + def test_merge_where_one_file_is_empty(self): + plist1 = _xml_plist('Fooabc') + plist2 = _xml_plist('') + self._assert_plisttool_result({'plists': [plist1, plist2]}, {'Foo': 'abc'}) + + def test_merge_where_one_dict_is_empty(self): + plist1 = {'Foo': 'abc'} + plist2 = {} + self._assert_plisttool_result({'plists': [plist1, plist2]}, {'Foo': 'abc'}) + + def test_merge_where_both_files_are_empty(self): + plist1 = _xml_plist('') + plist2 = _xml_plist('') + self._assert_plisttool_result({'plists': [plist1, plist2]}, {}) + + def test_merge_where_both_dicts_are_empty(self): + plist1 = {} + plist2 = {} + self._assert_plisttool_result({'plists': [plist1, plist2]}, {}) + + def test_more_complicated_merge(self): + plist1 = _xml_plist( + 'String1abc' + 'Integer1123' + 'Array1ab' + ) + plist2 = _xml_plist( + 'String2def' + 'Integer2987' + 'Dictionary2' + 'k1a' + 'k2b' + '' + ) + plist3 = _xml_plist( + 'String3ghi' + 'Integer3465' + 'Bundlethis.is.${BUNDLE_NAME}.bundle' + ) + self._assert_plisttool_result({ + 'plists': [plist1, plist2, plist3], + 'info_plist_options': { + 'bundle_name': 'my' + }, + }, { + 'String1': 'abc', + 'Integer1': 123, + 'Array1': ['a', 'b'], + 'String2': 'def', + 'Integer2': 987, + 'Dictionary2': {'k1': 'a', 'k2': 'b'}, + 'String3': 'ghi', + 'Integer3': 465, + 'Bundle': 'this.is.my.bundle', + }) + + def test_merge_with_forced_plist_overrides_on_collisions(self): + plist1 = {'Foo': 'bar'} + plist2 = {'Foo': 'baz'} + self._assert_plisttool_result({ + 'plists': [plist1], + 'forced_plists': [plist2], + }, {'Foo': 'baz'}) + + def test_merge_with_forced_plists_with_same_key_keeps_last_one(self): + plist1 = {'Foo': 'bar'} + plist2 = {'Foo': 'baz'} + plist3 = {'Foo': 'quux'} + self._assert_plisttool_result({ + 'plists': [plist1], + 'forced_plists': [plist2, plist3], + }, {'Foo': 'quux'}) + + def test_executable_is_added_if_key_not_present(self): + plist1 = _xml_plist('Fooabc') + self._assert_plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'executable': 'MyApp', + }, + }, { + 'Foo': 'abc', + 'CFBundleExecutable': 'MyApp', + }) + + def test_executable_is_overwritten_if_key_is_present(self): + plist1 = _xml_plist('CFBundleExecutableabc') + self._assert_plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'executable': 'MyApp', + }, + }, { + 'CFBundleExecutable': 'MyApp', + }) + + def test_executable_name_substitutions(self): + plist1 = _xml_plist( + 'FooBraces${EXECUTABLE_NAME}' + 'BarBraces${PRODUCT_NAME}' + 'FooParens$(EXECUTABLE_NAME)' + 'BarParens$(PRODUCT_NAME)' + ) + outdict = _plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'executable': 'MyApp', + }, + }) + self.assertEqual('MyApp', outdict.get('FooBraces')) + self.assertEqual('MyApp', outdict.get('BarBraces')) + self.assertEqual('MyApp', outdict.get('FooParens')) + self.assertEqual('MyApp', outdict.get('BarParens')) + + def test_bundle_name_substitutions(self): + plist1 = _xml_plist( + 'FooBraces${BUNDLE_NAME}' + 'FooParens$(BUNDLE_NAME)' + ) + outdict = _plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'bundle_name': 'MyBundle', + }, + }) + self.assertEqual('MyBundle', outdict.get('FooBraces')) + self.assertEqual('MyBundle', outdict.get('FooParens')) + + def test_rfc1034_conversion(self): + plist1 = _xml_plist( + 'Foo${PRODUCT_NAME:rfc1034identifier}' + ) + outdict = _plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'executable': 'foo_bar?baz' + }, + }) + self.assertEqual('foo-bar-baz', outdict.get('Foo')) + + def test_nonexistant_substitution(self): + plist1 = _xml_plist( + 'FooBraces${NOT_A_VARIABLE}' + 'FooParens$(NOT_A_VARIABLE)' + ) + outdict = _plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'executable': 'MyApp', + 'bundle_name': 'MyBundle', + }, + }) + self.assertEqual('${NOT_A_VARIABLE}', outdict.get('FooBraces')) + self.assertEqual('$(NOT_A_VARIABLE)', outdict.get('FooParens')) + + def test_recursive_substitutions(self): + plist1 = _xml_plist( + 'Foo' + '' + ' Foo1' + ' ${BUNDLE_NAME}' + ' Foo2' + ' ' + ' ${BUNDLE_NAME}' + ' ' + '' + 'Bar' + '' + ' ${BUNDLE_NAME}' + ' ' + ' Baz' + ' ${BUNDLE_NAME}' + ' ' + '' + ) + outdict = _plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'bundle_name': 'MyBundle', + }, + }) + self.assertEqual('MyBundle', outdict.get('Foo').get('Foo1')) + self.assertEqual('MyBundle', outdict.get('Foo').get('Foo2')[0]) + self.assertEqual('MyBundle', outdict.get('Bar')[0]) + self.assertEqual('MyBundle', outdict.get('Bar')[1].get('Baz')) + + def test_keys_with_same_values_do_not_raise_error(self): + plist1 = _xml_plist('FooBar') + plist2 = _xml_plist('FooBar') + self._assert_plisttool_result({'plists': [plist1, plist2]}, {'Foo': 'Bar'}) + + def test_conflicting_keys_raises_error(self): + with self.assertRaises(plisttool.PlistConflictError) as context: + plist1 = _xml_plist('FooBar') + plist2 = _xml_plist('FooBaz') + _plisttool_result({'plists': [plist1, plist2]}) + + self.assertEqual('Foo', context.exception.key) + # Don't care about the order of the values. + values = set([context.exception.value1, context.exception.value2]) + self.assertIn('Bar', values) + self.assertIn('Baz', values) + + def test_order_of_elements_in_one_plist_merge_is_maintained(self): + """Verify that we merge keys in the same order as the original dictionary. + + Ordering of keys is important in build caching -- the ordering must be + deterministic. Xcode, which is built on Foundation's non-order-preserving + NSDictionary, is harmful to caching because it merges keys in arbitrary + order. + + We do our merging in insertion order, which is deterministic. The plistlib + module reads plists into a standard Python dictionary, which is hashed, and + then we immediately copy the entries into an OrderedDict. The original + arbitrary ordering initially seems problematic, but we can assume that two + dictionaries that are built by inserting the same keys in the same order, + without any intervening removals, will produce the same hashtable structure + and would be iterated in the same order (this matters when building the + OrderedDict). From that point on, we operate only on OrderedDicts, which + preserves the order, and finally, writePlist iterates over the OrderedDict + and writes the XML plist back out in that order. + """ + random.seed(8675309) + key_order = OrderedDict() + source_plist = OrderedDict() + for _ in range(50000): + key = 'key#%d' % random.randint(0, 2 ** 32 - 1) + key_order[key] = True + source_plist[key] = 'value' + + outdict = OrderedDict() + tool = plisttool.PlistTool({}) + tool.merge_dictionaries(source_plist, outdict) + + self.assertEqual(outdict.keys(), key_order.keys()) + + def test_bundle_id_in_plist_does_not_raise_error(self): + output = StringIO.StringIO() + control = { + 'plists': [{'CFBundleIdentifier': 'foo.bar.baz'}], + 'output': output + } + tool = plisttool.PlistTool(control) + tool.run() + + def test_bundle_id_not_in_plist_but_overridden_does_not_raise_error(self): + plist1 = _xml_plist('Fooabc') + self._assert_plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'bundle_id': 'foo.bar.baz' + }, + }, { + 'Foo': 'abc', + 'CFBundleIdentifier': 'foo.bar.baz' + }) + + def test_bundle_id_mismatch_raises_error(self): + with self.assertRaisesRegexp( + ValueError, + plisttool.MISMATCHED_BUNDLE_ID_MSG % ('abc', 'foo.bar.baz')): + plist1 = _xml_plist('CFBundleIdentifierabc') + _plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'bundle_id': 'foo.bar.baz' + }, + }) + + def test_bundle_id_is_replaced_by_override(self): + plist1 = _xml_plist('CFBundleIdentifier' + '${PRODUCT_BUNDLE_IDENTIFIER}') + self._assert_plisttool_result({ + 'plists': [plist1], + 'info_plist_options': { + 'bundle_id': 'foo.bar.baz' + }, + }, {'CFBundleIdentifier': 'foo.bar.baz'}) + + def test_pkginfo_with_valid_values(self): + self._assert_pkginfo({ + 'CFBundlePackageType': 'APPL', + 'CFBundleSignature': '1234', + }, 'APPL1234') + + def test_pkginfo_with_missing_package_type(self): + self._assert_pkginfo({ + 'CFBundleSignature': '1234', + }, '????1234') + + def test_pkginfo_with_missing_signature(self): + self._assert_pkginfo({ + 'CFBundlePackageType': 'APPL', + }, 'APPL????') + + def test_pkginfo_with_missing_package_type_and_signature(self): + self._assert_pkginfo({}, '????????') + + def test_pkginfo_with_values_too_long(self): + self._assert_pkginfo({ + 'CFBundlePackageType': 'APPLE', + 'CFBundleSignature': '1234', + }, '????1234') + + def test_pkginfo_with_valid_values_too_short(self): + self._assert_pkginfo({ + 'CFBundlePackageType': 'APPL', + 'CFBundleSignature': '123', + }, 'APPL????') + + def test_pkginfo_with_values_encodable_in_mac_roman(self): + self._assert_pkginfo({ + 'CFBundlePackageType': u'ÄPPL', + 'CFBundleSignature': '1234', + }, '\x80PPL1234') + + def test_pkginfo_with_values_not_encodable_in_mac_roman(self): + self._assert_pkginfo({ + 'CFBundlePackageType': u'😎', + 'CFBundleSignature': '1234', + }, '????1234') + + def test_child_plist_that_matches_parent_does_not_raise(self): + parent = _xml_plist( + 'CFBundleIdentifierfoo.bar' + 'CFBundleShortVersionString1.2.3') + child = _xml_plist( + 'CFBundleIdentifierfoo.bar.baz' + 'CFBundleShortVersionString1.2.3') + children = {'//fake:label': child} + _plisttool_result({ + 'plists': [parent], + 'info_plist_options': { + 'child_plists': children, + }, + }) + + def test_child_plist_with_incorrect_bundle_id_raises(self): + with self.assertRaises(ValueError): + parent = _xml_plist( + 'CFBundleIdentifierfoo.bar' + 'CFBundleShortVersionString1.2.3') + child = _xml_plist( + 'CFBundleIdentifierfoo.baz' + 'CFBundleShortVersionString1.2.3') + children = {'//fake:label': child} + _plisttool_result({ + 'plists': [parent], + 'info_plist_options': { + 'child_plists': children, + }, + }) + + def test_child_plist_with_incorrect_bundle_version_raises(self): + with self.assertRaises(ValueError): + parent = _xml_plist( + 'CFBundleIdentifierfoo.bar' + 'CFBundleShortVersionString1.2.3') + child = _xml_plist( + 'CFBundleIdentifierfoo.bar.baz' + 'CFBundleShortVersionString1.2.4') + children = {'//fake:label': child} + _plisttool_result({ + 'plists': [parent], + 'info_plist_options': { + 'child_plists': children, + }, + }) + + +if __name__ == '__main__': + unittest.main() diff --git a/apple/bundling/process_and_sign.sh.template b/apple/bundling/process_and_sign.sh.template new file mode 100644 index 0000000000..477d9c1fd2 --- /dev/null +++ b/apple/bundling/process_and_sign.sh.template @@ -0,0 +1,49 @@ +#!/bin/bash +set -eu + +# The path where the archive should be created, including the name of the +# archive itself. +readonly OUTPUT_PATH=%output_path% +# The name of the post processor executable, if any, that should be run on the +# directory containing the package contents. +readonly POST_PROCESSOR=%post_processor% +# Indicates whether the final archive should be compressed (1) or not (0). +readonly SHOULD_COMPRESS=%should_compress% +# The path to archive with the unprocessed and unsigned bundle. +readonly UNPROCESSED_ARCHIVE_PATH=%unprocessed_archive_path% +# The absolute path to the directory where the unprocessed archive will be +# extracted for post-processing and signing. +readonly WORK_DIR=%work_dir% + +# Blow away the old working dir if it's still there from a previous build. +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" + +# Expand the unprocessed archive into the temporary directory. +unzip -qq -d "$WORK_DIR" "$UNPROCESSED_ARCHIVE_PATH" + +# Execute the archive post-processor, if it was provided. +if [[ -n "$POST_PROCESSOR" ]]; then + "$POST_PROCESSOR" "$WORK_DIR" +fi + +# Sign the application, if requested. This may expand to nothing in the case +# where signing is not being performed. +%signing_command_lines% + +if [[ "$SHOULD_COMPRESS" == "1" ]]; then + readonly COMPRESSION_METHOD=deflate +else + readonly COMPRESSION_METHOD=store +fi + +# Create the final ZIP archive and copy it into its destination in bazel-bin/. +readonly OUTPUT_BASENAME=$(basename "$OUTPUT_PATH") +pushd "$WORK_DIR" >/dev/null +# Ensure that the entries in the ZIP are writable when expanded, because some +# tools rely on the ability to unzip it, modify it, and re-sign it. +chmod -R u+w . +( TZ=UTC find . -exec touch -h -t 198001010000 {} \+ ) +zip -qX -r --compression-method "$COMPRESSION_METHOD" "$OUTPUT_BASENAME" . +popd >/dev/null +mv "$WORK_DIR/$OUTPUT_BASENAME" "$OUTPUT_PATH" diff --git a/apple/bundling/product_actions.bzl b/apple/bundling/product_actions.bzl new file mode 100644 index 0000000000..386feb5d14 --- /dev/null +++ b/apple/bundling/product_actions.bzl @@ -0,0 +1,122 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions to manipulate support files for Apple product types.""" + +load("//apple/bundling:file_support.bzl", "file_support") +load("//apple/bundling:platform_support.bzl", + "platform_support") +load("//apple:utils.bzl", "bash_quote") + + +def _copy_stub_for_bundle(ctx, product_info): + """Registers an action that copies a stub executable from the SDK. + + Args: + ctx: The Skylark context. + product_info: The product type info struct. + Returns: + A `File` that is a copy of the stub executable and has the same name as the + bundle (just as a user-provided binary would). + """ + file_path = '"' + product_info.stub_path + '"' + stub_binary = file_support.intermediate(ctx, "%{name}.stub_binary") + + platform, _ = platform_support.platform_and_sdk_version(ctx) + platform_name = platform.name_in_plist + + platform_support.xcode_env_action( + ctx, + inputs=[], + outputs=[stub_binary], + command=[ + "/bin/bash", "-c", + ("set -e && " + + "PLATFORM_DIR=\"${{DEVELOPER_DIR}}/Platforms/" + + "{platform_name}.platform\" && " + + "cp {file_path} {stub_binary}" + ).format( + file_path=file_path, + platform_name=platform_name, + stub_binary=stub_binary.path, + ), + ], + mnemonic="CopyStubExecutable", + no_sandbox=True, + ) + + return stub_binary + + +def _create_stub_zip_for_archive_merging(ctx, product_info): + """Registers an action that creates a ZIP of a stub executable. + + When uploading an archive to Apple, product types that involve stub + executables need those executables copied into appropriate locations inside + the archive root. This function creates a ZIP with the correct file structure + so that it can be propagated up to an application archive for merging. + + Args: + ctx: The Skylark context. + product_info: The product type info struct. + Returns: + A `File` that is the zip that should be merged into the archive root. + """ + product_support_zip = ctx.new_file(ctx.label.name + "-Support.zip") + product_support_path = bash_quote(product_support_zip.path) + product_support_basename = product_support_zip.basename + file_path = '"' + product_info.stub_path + '"' + archive_path = bash_quote(product_info.archive_path) + + platform, _ = platform_support.platform_and_sdk_version(ctx) + platform_name = platform.name_in_plist + + # TODO(b/23975430): Remove the /bin/bash workaround once this bug is fixed. + platform_support.xcode_env_action( + ctx, + inputs=[], + outputs=[product_support_zip], + command=[ + "/bin/bash", "-c", + ("set -e && " + + "PLATFORM_DIR=\"${{DEVELOPER_DIR}}/Platforms/" + + "{platform_name}.platform\" && " + + "ZIPDIR=$(mktemp -d \"${{TMPDIR:-/tmp}}/support.XXXXXXXXXX\") && " + + "trap \"rm -r ${{ZIPDIR}}\" EXIT && " + + "pushd ${{ZIPDIR}} >/dev/null && " + + "mkdir -p $(dirname {archive_path}) && " + + "cp {file_path} {archive_path} && " + + "zip -qX -r {product_support_basename} . && " + + "popd >/dev/null && " + + "mv ${{ZIPDIR}}/{product_support_basename} {product_support_path}" + ).format( + archive_path=archive_path, + file_path=file_path, + platform_name=platform_name, + product_support_basename=product_support_basename, + product_support_path=product_support_path, + ), + ], + mnemonic = "ZipStubExecutable", + no_sandbox=True, + ) + + return product_support_zip + + +# Define the loadable module that lists the exported symbols in this file. +product_actions = struct( + copy_stub_for_bundle=_copy_stub_for_bundle, + create_stub_zip_for_archive_merging=_create_stub_zip_for_archive_merging, +) diff --git a/apple/bundling/product_support.bzl b/apple/bundling/product_support.bzl new file mode 100644 index 0000000000..799e5de77b --- /dev/null +++ b/apple/bundling/product_support.bzl @@ -0,0 +1,166 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support for product types used by Apple bundling rules. + +This file should be loaded by the top-level Apple platform .bzl files +(ios.bzl, watchos.bzl, and so forth) and should export *only* the +`apple_product_type` struct so that BUILD files can import it through there +and access the constants in their own targets. +""" + + +# Only product types that are meant to be user-specified should be exported +# here. +apple_product_type = struct( + messages_application="com.apple.product-type.application.messages", + messages_extension="com.apple.product-type.app-extension.messages", + messages_sticker_pack_extension=( + "com.apple.product-type.app-extension.messages-sticker-pack"), +) +""" +Product type identifiers used by special application and extension types. + +Some applications and extensions, such as iMessage applications and +sticker packs in iOS 10, receive special treatment when building (for example, +bundling a stub executable instead of a user-defined binary, or extra arguments +passed to tools like the asset compiler). These behaviors are captured in the +product type identifier. The product types currently supported are: + +* `messages_application`: An application that integrates with the Messages + app (iOS 10 and above). This application must include an `ios_extension` + with the `messages_extension` or `messages_sticker_pack_extension` product + type (or both extensions). This product type does not contain a user-provided + binary. +* `messages_extension`: An extension that integrates custom code/behavior into + a Messages application. This product type should contain a user-provided + binary. +* `messages_sticker_pack_extension`: An extension that defines custom sticker + packs for the Messages app. This product type does not contain a + user-provided binary. +""" + + +# Private declarations of the product types for watchOS 2+ apps and extensions. +# Exported for the benefit of watchos.bzl but not intended to be used directly +# by end-users. +_WATCHAPP2_PRODUCT_TYPE = "com.apple.product-type.application.watchapp2" +_WATCHKIT2_EXTENSION_PRODUCT_TYPE = ( + "com.apple.product-type.watchkit2-extension") + + +# Watch applications and some iOS extensions (like message sticker packs) do +# not include source code of their own and require stub binaries copied in from +# the platform SDK. See the docstring for `_stub_binary_info_for_target` for +# the meaning of these struct fields. +_PRODUCT_TYPE_INFO_MAP = { + apple_product_type.messages_application: struct( + stub_path=("${PLATFORM_DIR}/Library/Application Support/" + + "MessagesApplicationStub/MessagesApplicationStub"), + archive_path=("MessagesApplicationSupport/" + + "MessagesApplicationSupportStub"), + bundle_path=None, + additional_infoplist_values={ + "LSApplicationLaunchProhibited": True, + }, + ), + apple_product_type.messages_sticker_pack_extension: struct( + stub_path=("${PLATFORM_DIR}/Library/Application Support/" + + "MessagesApplicationExtensionStub/" + + "MessagesApplicationExtensionStub"), + archive_path=("MessagesApplicationExtensionSupport/" + + "MessagesApplicationExtensionSupportStub"), + bundle_path=None, + additional_infoplist_values={ + "LSApplicationIsStickerPack": True, + }, + ), + _WATCHAPP2_PRODUCT_TYPE: struct( + stub_path="${SDKROOT}/Library/Application Support/WatchKit/WK", + archive_path="WatchKitSupport2/WK", + bundle_path="_WatchKitStub/WK", + additional_infoplist_values=None, + ), +} + + +def _product_type(ctx): + """Returns the product type identifier for the current target. + + Args: + ctx: The Skylark context. + Returns: + The product type identifier for the current target, or None if there is + none. + """ + if hasattr(ctx.attr, "product_type"): + return ctx.attr.product_type + + return getattr(ctx.attr, "_product_type", None) + + +def _product_type_info(product_type): + """Returns the stub binary info for the given product type. + + Args: + product_type: The product type. + Returns: + The info about the stub executable, or None if the target's product type + does not use a stub executable (meaning it requires a user binary). If not + None, the returned value is a struct with the following fields: + + * `stub_path`, which is the path (prefixed with an environment variable + like `${SDKROOT}`) from which the stub should be copied; + * `archive_path`, which is the support path at the archive root at which + the stub should be placed; and + * `bundle_path`, which is an additional bundle-relative location where the + stub should be copied (in addition to the bundle's binary itself). + * `additional_infoplist_values`, which is a dictionary of additional + key/value pairs that should be merged into the Info.plist for a bundle + with this product type. + """ + return _PRODUCT_TYPE_INFO_MAP.get(product_type) + + +def _product_type_info_for_target(ctx): + """Returns the stub binary info for a target's product type. + + Args: + ctx: The Skylark context. + Returns: + The info about the stub executable, or None if the target's product type + does not use a stub executable (meaning it requires a user binary). If not + None, the returned value is a struct with the following fields: + + * `file`, which is the path (prefixed with an environment variable like + `${SDKROOT}`) from which the stub should be copied; + * `archive_path`, which is the support path at the archive root at which + the stub should be placed; and + * `bundle_path`, which is an additional bundle-relative location where the + stub should be copied (in addition to the bundle's binary itself). + """ + product_type = _product_type(ctx) + if product_type: + return _product_type_info(product_type) + return None + + +# Define the loadable module that lists the exported symbols in this file. +product_support = struct( + WATCHAPP2_PRODUCT_TYPE=_WATCHAPP2_PRODUCT_TYPE, + WATCHKIT2_EXTENSION_PRODUCT_TYPE=_WATCHKIT2_EXTENSION_PRODUCT_TYPE, + product_type=_product_type, + product_type_info=_product_type_info, + product_type_info_for_target=_product_type_info_for_target, +) diff --git a/apple/bundling/provider_support.bzl b/apple/bundling/provider_support.bzl new file mode 100644 index 0000000000..f3a38ef38b --- /dev/null +++ b/apple/bundling/provider_support.bzl @@ -0,0 +1,60 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions for working with providers in build rules.""" + + +def _binary_or_deps_providers(ctx, name): + """Returns the providers with the given name for a target's binary or deps. + + Some Apple bundling rules unconditionally take a binary, some conditionally + can take a binary or omit it, and some unconditionally don't take a binary + under any circumstances. Because of these differences, we may need to access + dependencies' providers from either the `binary` attribute or the `deps`. + This function checks for the existence of a binary first and, if present, + returns the matching providers from that target. Otherwise, it returns the + matching providers from the direct `deps` of that target. (It is the + responsibility of those deps' rules/aspects to propagate information + transitively as needed.) + + Args: + ctx: The Skylark context. + name: The name of the provider to return. + Returns: + A list of providers from the current target's binary or deps. + """ + if hasattr(ctx.attr, "binary") and ctx.attr.binary: + return _matching_providers([ctx.attr.binary], name) + return _matching_providers(ctx.attr.deps, name) + + +def _matching_providers(targets, name): + """Returns a list of providers with the given name from a list of targets. + + Args: + targets: The list of targets whose providers should be searched. + name: The name of the provider to return. + Returns: + A list of providers from the given targets. This list may have fewer + elements than `targets` (including being empty) if not all targets + propagate the named provider. + """ + return [getattr(x, name) for x in targets if hasattr(x, name)] + + +# Define the loadable module that lists the exported symbols in this file. +provider_support = struct( + binary_or_deps_providers=_binary_or_deps_providers, + matching_providers=_matching_providers, +) diff --git a/apple/bundling/resource_actions.bzl b/apple/bundling/resource_actions.bzl new file mode 100644 index 0000000000..fc891501db --- /dev/null +++ b/apple/bundling/resource_actions.bzl @@ -0,0 +1,636 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions used to process resources in Apple bundles.""" + +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:file_support.bzl", "file_support") +load("//apple/bundling:platform_support.bzl", + "platform_support") +load("//apple/bundling:product_support.bzl", + "apple_product_type", + "product_support") +load("//apple/bundling:resource_support.bzl", + "resource_support") +load("//apple:utils.bzl", + "basename", + "bash_array_string", + "group_files_by_directory", + "intersperse", + "optionally_prefixed_path", + "remove_extension", + "replace_extension", + "split_extension") +load("//apple:utils.bzl", "xcrun_action") + + +# Sentinel value used as the key in the dictionary returned by +# `group_resources` whose value is the set of files that didn't satisfy any of +# the groupings. +_UNGROUPED = "" + + +def _group_resources(files, groupings): + """Groups files based on their directory or file extension. + + This function does not directly use the `group_files_by_directory` helper + function; it is implemented in such a way that it only requires one pass + through the `files` set, since walking Skylark sets can be slow. + + Args: + files: The set of `File` objects representing resources that should be + grouped. + groupings: A list of strings denoting file or directory extensions by which + groups should be created. The extensions do not contain the leading + dot. If a string ends with a slash (such as "xcassets/"), then a + grouping is created that contains all files under directories with + that extension, regardless of the individual files' extensions. If a + string does not end with a slash (such as "xib"), then a grouping is + created that contains all files with that extension. If a file would + satisfy two different groupings (for example, a file in an "xcassets/" + directory that has an extension in the list), then the directory + grouping takes precedence. + Returns: + A dictionary whose keys are the groupings from `groupings` and the values + are sets of `File` objects that are in that grouping. An additional key, + `_UNGROUPED`, contains files that did not satisfy any of the groupings. In + other words, the union of all the sets in the returned dictionary is equal + to `resources` and the intersection of any two sets is empty. + """ + grouped_files = {g: depset() for g in groupings} + grouped_files[_UNGROUPED] = depset() + + # Pull out the directory groupings because we need to actually iterate over + # these to find matches. + dir_groupings = [g for g in groupings if g.endswith("/")] + + for f in files: + path = f.path + + # Try to find a directory-based match first. + matched_group = None + for extension_candidate in dir_groupings: + search_string = "." + extension_candidate + if search_string in path: + matched_group = extension_candidate + break + + # If no directory match was found, use the file's extension to group it. + if not matched_group: + _, extension = split_extension(path) + # Strip the leading dot. + extension = extension[1:] + matched_group = extension if extension in grouped_files else _UNGROUPED + + grouped_files[matched_group] = grouped_files[matched_group] | [f] + + return grouped_files + + +def _compile_strings(ctx, strings_file, resource_info): + """Creates an action that converts a strings plist to binary format. + + Args: + ctx: The Skylark context. + strings_file: The .strings file that should be converted. + resource_info: A struct returned by `resource_support.resource_info` that + contains information needed by the resource processing functions. + Returns: + A struct as defined by `_process_resources` that will be merged with those + from other processing functions. + """ + bundle_dir = resource_info.bundle_dir + + path = resource_support.lproj_rooted_path_or_basename(strings_file) + out_file = file_support.intermediate( + ctx, "%{name}.resources/" + path, bundle_dir) + + xcrun_action( + ctx, + inputs=[strings_file], + outputs=[out_file], + arguments=[ + "/usr/bin/plutil", + "-convert", "binary1", + "-o", out_file.path, + "--", strings_file.path, + ], + mnemonic="CompileStrings", + ) + + full_bundle_path = optionally_prefixed_path(path, bundle_dir) + return struct( + bundle_merge_files=depset([ + bundling_support.resource_file(ctx, out_file, full_bundle_path) + ]), + ) + + +def _actool_args_for_special_file_types(ctx, asset_catalogs, resource_info): + """Returns command line arguments needed to compile special assets. + + This function is called by `_actool` to scan for specially recognized asset + types, such as app icons and launch images, and determine any extra command + line arguments that need to be passed to `actool` to handle them. It also + checks the validity of those assets, if any (for example, by permitting only + one app icon set or launch image set to be present). + + Args: + ctx: The Skylark context. + asset_catalogs: The asset catalog files. + resource_info: A struct returned by `resource_support.resource_info` that + contains information needed by the resource processing functions. + Returns: + An array of extra arguments to pass to `actool`, which may be empty. + """ + args = [] + + product_type = product_support.product_type(ctx) + if product_type in (apple_product_type.messages_extension, + apple_product_type.messages_sticker_pack_extension): + appicon_extension = "stickersiconset" + icon_files = [f for f in asset_catalogs if ".stickersiconset/" in f.path] + + args.extend([ + "--sticker-pack-identifier-prefix", + resource_info.bundle_id + ".sticker-pack." + ]) + + # Fail if the user has included .appiconset folders in their asset catalog; + # Message extensions must use .stickersiconset instead. + appiconset_files = [f for f in asset_catalogs if ".appiconset/" in f.path] + if appiconset_files: + appiconset_dirs = group_files_by_directory(appiconset_files, + ["appiconset"], + attr="app_icons").keys() + formatted_dirs = "[\n %s\n]" % ",\n ".join(appiconset_dirs) + fail("Message extensions must use Messages Extensions Icon Sets " + + "(named .stickersiconset), not traditional App Icon Sets " + + "(.appiconset). Found the following: " + + formatted_dirs, "app_icons") + else: + appicon_extension = "appiconset" + icon_files = [f for f in asset_catalogs if ".appiconset/" in f.path] + + # Add arguments for app icons, if there are any. + if icon_files: + icon_dirs = group_files_by_directory(icon_files, + [appicon_extension], + attr="app_icons").keys() + if len(icon_dirs) != 1: + formatted_dirs = "[\n %s\n]" % ",\n ".join(icon_dirs) + fail("The asset catalogs should contain exactly one directory named " + + "*.%s among its asset catalogs, " % appicon_extension + + "but found the following: " + formatted_dirs, "app_icons") + + app_icon_name = remove_extension(basename(icon_dirs[0])) + args += ["--app-icon", app_icon_name] + + # Add arguments for launch images, if there are any. + launch_image_files = [f for f in asset_catalogs if ".launchimage/" in f.path] + if launch_image_files: + launch_image_dirs = group_files_by_directory(launch_image_files, + ["launchimage"], + attr="launch_images").keys() + if len(launch_image_dirs) != 1: + formatted_dirs = "[\n %s\n]" % ",\n ".join(launch_image_dirs) + fail("The asset catalogs should contain exactly one directory named " + + "*.launchimage among its asset catalogs, but found the " + + "following: " + formatted_dirs, "launch_images") + + launch_image_name = remove_extension(basename(launch_image_dirs[0])) + args += ["--launch-image", launch_image_name] + + return args + + +def _actool(ctx, asset_catalogs, resource_info): + """Creates an action that compiles asset catalogs. + + This action produces an .actool.zip file containing compiled assets that must + be merged into the application/extension bundle. It also produces a partial + Info.plist that must be merged info the application's main plist if an app + icon or launch image are requested (if not, the actool plist is empty). + + Args: + ctx: The Skylark context. + asset_catalogs: An iterable of files in all asset catalogs that should be + packaged as part of the application. This should include transitive + dependencies (i.e., assets not just from the application target, but + from any other library targets it depends on) as well as resources like + app icons and launch images. + resource_info: A struct returned by `resource_support.resource_info` that + contains information needed by the resource processing functions. + Returns: + A struct as defined by `_process_resources` that will be merged with those + from other processing functions. + """ + bundle_dir = resource_info.bundle_dir + + out_zip = file_support.intermediate(ctx, "%{name}.actool.zip", bundle_dir) + out_plist = file_support.intermediate( + ctx, "%{name}.actool-PartialInfo.plist", bundle_dir) + + platform, _ = platform_support.platform_and_sdk_version(ctx) + min_os = platform_support.minimum_os(ctx) + actool_platform = platform.name_in_plist.lower() + + args = [ + out_zip.path, + "--platform", actool_platform, + "--output-partial-info-plist", out_plist.path, + "--minimum-deployment-target", str(min_os), + "--compress-pngs", + ] + + product_type = product_support.product_type(ctx) + if product_type: + args.extend(["--product-type", product_type]) + args.extend(_actool_args_for_special_file_types( + ctx, asset_catalogs, resource_info)) + args.extend(intersperse("--target-device", platform_support.families(ctx))) + + xcassets = group_files_by_directory(asset_catalogs, + ["xcassets", "xcstickers"], + attr="asset_catalogs").keys() + args.extend(xcassets) + + platform_support.xcode_env_action( + ctx, + inputs=list(asset_catalogs), + outputs=[out_zip, out_plist], + executable=ctx.executable._actoolwrapper, + arguments=args, + mnemonic="AssetCatalogCompile", + no_sandbox=True, + ) + + return struct( + bundle_merge_zips=depset([ + bundling_support.resource_file(ctx, out_zip, bundle_dir) + ]), + partial_infoplists=depset([out_plist]), + ) + + +def _ibtool_arguments(ctx): + """Returns common `ibtool` command line arguments. + + This function returns the common arguments used by both xib and storyboard + compilation, as well as storyboard linking. Callers should add their own + arguments to the returned array for their specific purposes. + + Args: + ctx: The Skylark context. + Returns: + An array of command-line arguments to pass to ibtool. + """ + min_os = platform_support.minimum_os(ctx) + + return [ + "--minimum-deployment-target", str(min_os), + ] + intersperse("--target-device", platform_support.families(ctx)) + + +def _ibtool_compile(ctx, input_file, resource_info): + """Creates an action that compiles a storyboard or xib file. + + Args: + ctx: The Skylark context. + input_file: The storyboard or xib file to compile. + resource_info: A struct returned by `resource_support.resource_info` that + contains information needed by the resource processing functions. + Returns: + A struct as defined by `_process_resources` that will be merged with those + from other processing functions. + """ + bundle_dir = resource_info.bundle_dir + swift_module = resource_info.swift_module + + path = resource_support.lproj_rooted_path_or_basename(input_file) + + if path.endswith(".storyboard"): + is_storyboard = True + mnemonic = "StoryboardCompile" + out_name = replace_extension(path, ".storyboardc") + else: + is_storyboard = False + mnemonic = "XibCompile" + out_name = replace_extension(path, ".nib") + + out_file = file_support.intermediate( + ctx, "%{name}.resources/" + path + ".zip", bundle_dir) + + # The first two arguments are those required by ibtoolwrapper; the remaining + # ones are passed to ibtool verbatim. + args = [out_file.path, out_name] + _ibtool_arguments(ctx) + [ + "--module", swift_module or ctx.label.name, + input_file.path + ] + + platform_support.xcode_env_action( + ctx, + inputs=[input_file], + outputs=[out_file], + executable=ctx.executable._ibtoolwrapper, + arguments=args, + mnemonic=mnemonic, + no_sandbox=True, + ) + + if is_storyboard: + return struct(compiled_storyboards=depset([out_file])) + else: + return struct(bundle_merge_zips=depset([ + bundling_support.resource_file(ctx, out_file, bundle_dir) + ])) + + +def _ibtool_link(ctx, storyboardc_zips): + """Creates an action that links multiple compiled storyboards. + + Storyboards that reference each other must be linked, and this operation also + copies them into a directory structure matching that which should appear in + the final bundle. + + Args: + ctx: The Skylark context. + storyboardc_zips: A list of zipped, compiled storyboards (produced by + `resource_actions.ibtool_compile`) that should be linked. + Returns: + The File object representing the ZIP file containing the linked + storyboards. + """ + out_zip = file_support.intermediate( + ctx, "%{name}.resources/linked-storyboards.zip") + + # The first two arguments are those required by ibtoolwrapper; the remaining + # ones are passed to ibtool verbatim. + args = ([out_zip.path, "", "--link"] + _ibtool_arguments(ctx) + + [f.path for f in storyboardc_zips]) + + platform_support.xcode_env_action( + ctx, + inputs=storyboardc_zips, + outputs=[out_zip], + executable=ctx.executable._ibtoolwrapper, + arguments=args, + mnemonic="StoryboardLink", + no_sandbox=True, + ) + + return out_zip + + +def _momc(ctx, input_files, resource_info): + """Creates actions that compile a Core Data data model files. + + Each file should be contained inside a .xcdatamodel(d) directory. + .xcdatamodel directories that are not contained inside a .xcdatamodeld (note + the extra "d") directory are unversioned data models; those contained inside + a .xcdatamodeld directory are versioned data models. One compilation action + will be created for each .xcdatamodel directory except for those inside + .xcdatamodeld directories; in that case, one action will be created for each + of the .xcdatamodeld directories instead. + + Args: + ctx: The Skylark context. + input_files: An iterable of files in all data models that should be + compiled and packaged as part of the application. + resource_info: A struct returned by `resource_support.resource_info` that + contains information needed by the resource processing functions. + Returns: + A struct as defined by `_process_resources` that will be merged with those + from other processing functions. + """ + + # Before grouping the files by .xcdatamodel, we need to filter out any + # .xccurrentversion files that might be present in versioned data models. We + # add them back when the individual actions are registered. + bundle_dir = resource_info.bundle_dir + swift_module = resource_info.swift_module + + xccurrentversions = {f.dirname:f for f in input_files + if f.basename == ".xccurrentversion"} + inputs_without_versions = [f for f in input_files + if f.dirname not in xccurrentversions] + + models_to_compile = {} + grouped_models = group_files_by_directory(inputs_without_versions, + ["xcdatamodel"], + attr="datamodels") + + # If there are any .xcdatamodel directories that are contained in an + # .xcdatamodeld directory, fold them all into a single entry keyed by that + # containing directory. + for model, children in grouped_models.items(): + if ".xcdatamodeld/" in model: + name, extension, _ = model.rpartition(".xcdatamodeld/") + xcdatamodeld = name + extension[:-1] + if xcdatamodeld not in models_to_compile: + models_to_compile[xcdatamodeld] = depset() + + models_to_compile[xcdatamodeld] += children + else: + models_to_compile[model] = children + + out_files = [] + + platform, _ = platform_support.platform_and_sdk_version(ctx) + platform_name = platform.name_in_plist.lower() + deployment_target_option = "--%s-deployment-target" % platform_name + min_os = platform_support.minimum_os(ctx) + + for model, children in models_to_compile.items(): + # Add the .xccurrentversion file back if this was a versioned model so that + # it gets included as an input to the action. + if model in xccurrentversions: + children += depset([xccurrentversions[model]]) + + model_name = remove_extension(basename(model)) + extension = ".momd" if model.endswith(".xcdatamodeld") else ".mom" + archive_root_dir = model_name + extension + + out_file = file_support.intermediate( + ctx, "%%{name}.%s%s.zip" % (model_name, extension), bundle_dir) + out_files.append(out_file) + + args = [ + out_file.path, + archive_root_dir, + deployment_target_option, str(min_os), + "--module", swift_module or ctx.label.name, + model, + ] + + platform_support.xcode_env_action( + ctx, + inputs=list(children), + outputs=[out_file], + executable=ctx.executable._momcwrapper, + arguments=args, + mnemonic="MomCompile", + ) + + return struct( + bundle_merge_zips=depset([ + bundling_support.resource_file(ctx, f, bundle_dir) for f in out_files + ]), + ) + + +# The arity of a resource processing function, which denotes whether the +# matching files should be processed individually (`each`) or by a single +# action (`all`). +_arity = struct( + all="all", + each="each", +) + + +# A list of tuples describing resource types and how they should be processed. +# Each tuple must contain exactly three elements: +# +# 1. The file or directory extension corresponding to a type of resource. +# Directory extensions should be indicated with a trailing slash. Extensions +# should not have a leading dot. +# 2. The "arity" of the function that processes this type of resource. Legal +# values are `_arity.each`, which means that the function will be called +# separately for each file in the set; and `_arity.all`, which calls the +# function only once and passes it the entire set of files. +# 3. The function that should be called for resources of this type. +# +# The function described above takes three arguments: +# +# 1. The Skylark context (`ctx`). +# 2. A `File` object (if arity was `_arity.each`) or a set of `File` objects +# (if arity was `_arity.all`). +# 3. The resource info struct (as returned by `resource_support.resource_info`) +# containing additional information about the resources being processed. +# +# It should return the same `struct` described in the return type of the +# `_process_resources` function; the results across all invocations are merged. +# +# NOTE: The order of these entries matters for directory groupings, which +# is why this is expressed as a list instead of a dictionary. This is necessary +# to handle potentially tricky containment relationships properly. For example, +# Core Data models can be versioned (an .xcdatamodeld/ directory containing +# multiple .xcdatamodel/ directories) or unversioned (a standalone +# .xcdatamodel/ directory). In order to make sure that unversioned ones aren't +# processed independently of their parent, the "xcdatamodeld/" entry must +# appear first. The order of file-based groupings is unimportant, because those +# files are always looked up simply by their extension. +# +# Because this is being evaluated at global scope, it must remain *below* any +# of the functions to which it refers, but *above* _process_resources which +# refers to it. +_PROCESSABLE_RESOURCES = [ + # Asset catalogs. + (["xcassets/", "xcstickers/"], _arity.all, _actool), + # Core Data data models (versioned and unversioned). + (["xcdatamodeld/"], _arity.all, _momc), + (["xcdatamodel/"], _arity.all, _momc), + # Interface Builder files. + (["storyboard"], _arity.each, _ibtool_compile), + (["xib"], _arity.each, _ibtool_compile), + # Localizable strings. + (["strings"], _arity.each, _compile_strings), +] + + +def _process_resources(ctx, files, resource_info): + """Creates actions that process resources based on their extensions. + + Args: + ctx: The Skylark context. + files: The set of `File` objects representing resources that should be + processed. + resource_info: A struct returned by `resource_support.resource_info` that + contains information needed by the resource processing functions. + Returns: + A struct containing information that needs to be propagated back from + individual actions to the main bundler. It contains the following fields: + `bundle_merge_files`, a set of bundlable files that should be merged into + the bundle at specific locations; `bundle_merge_zips`, a set of bundlable + files that should be unzipped at specific locations in the bundle; + `compiled_storyboards`, a set of `File` objects representing compiled + storyboards that should be linked in the bundle; and `partial_infoplists`, + a set of `File` objects representing plists generated by resource + processing actions that should be merged into the bundle's final + Info.plist. + """ + # Get the flattened list of extensions across all processable resources types + # to pass to _group_resources. + extensions = [g[0] for g in _PROCESSABLE_RESOURCES] + extensions = [e for sublist in extensions for e in sublist] + grouped_files = _group_resources(files, extensions) + action_results = [] + + for (extensions, arity, function) in _PROCESSABLE_RESOURCES: + curr_files = depset() + for extension in extensions: + curr_files = curr_files + grouped_files[extension] + + if not curr_files: + continue + + if arity == _arity.all: + action_results.append(function(ctx, curr_files, resource_info)) + elif arity == _arity.each: + action_results.extend([function(ctx, f, resource_info) + for f in curr_files]) + else: + fail(("_process_resources is broken. Expected arity 'all' or 'each' " + + "but got '%s'") % arity) + + # Collect the results from the individual actions. + bundle_merge_files = depset() + bundle_merge_zips = depset() + compiled_storyboards = depset() + partial_infoplists = depset() + + for result in action_results: + bundle_merge_files = ( + bundle_merge_files | getattr(result, "bundle_merge_files", [])) + bundle_merge_zips = ( + bundle_merge_zips | getattr(result, "bundle_merge_zips", [])) + compiled_storyboards = ( + compiled_storyboards | getattr(result, "compiled_storyboards", [])) + partial_infoplists = ( + partial_infoplists | getattr(result, "partial_infoplists", [])) + + # Add any unprocessed resources to the list of files that will just be copied + # into the bundle. + unprocessed_resources = grouped_files[_UNGROUPED] + bundle_merge_files = bundle_merge_files | depset([ + bundling_support.resource_file(ctx, f, optionally_prefixed_path( + resource_support.lproj_rooted_path_or_basename(f), + resource_info.bundle_dir)) + for f in unprocessed_resources + ]) + + return struct( + bundle_merge_files=bundle_merge_files, + bundle_merge_zips=bundle_merge_zips, + compiled_storyboards=compiled_storyboards, + partial_infoplists=partial_infoplists, + ) + + +# Define the loadable module that lists the exported symbols in this file. +resource_actions = struct( + ibtool_link=_ibtool_link, + process_resources=_process_resources, +) diff --git a/apple/bundling/resource_support.bzl b/apple/bundling/resource_support.bzl new file mode 100644 index 0000000000..7d9c47deb5 --- /dev/null +++ b/apple/bundling/resource_support.bzl @@ -0,0 +1,64 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions used when processing resources in Apple bundles.""" + +load("//apple:utils.bzl", "basename") + + +def _lproj_rooted_path_or_basename(f): + """Returns an `.lproj`-rooted path for the given file if possible. + + If the file is nested in a `*.lproj` directory, then the `.lproj`-rooted path + to the file will be returned; for example, "fr.lproj/foo.strings". If the + file is not in a `*.lproj` directory, only the basename of the file is + returned. + + Args: + f: The `File` whose `.lproj`-rooted name or basename should be returned. + Returns: + The `.lproj`-rooted name or basename. + """ + if f.dirname.endswith(".lproj"): + filename = f.basename + dirname = basename(f.dirname) + return dirname + "/" + filename + + return f.basename + + +def _resource_info(bundle_id, bundle_dir="", swift_module=None): + """Returns an object to be passed to `resource_actions.process_resources`. + + Args: + bundle_id: The id of the bundle to which the resources belong. Required. + bundle_dir: The bundle directory that should be prefixed to any bundlable + files returned by the resource processing action. + swift_module: The name of the Swift module to which the resources belong, + if any. + Returns: + A struct that should be passed to `resource_actions.process_resources`. + """ + return struct( + bundle_dir=bundle_dir, + bundle_id=bundle_id, + swift_module=swift_module + ) + + +# Define the loadable module that lists the exported symbols in this file. +resource_support = struct( + lproj_rooted_path_or_basename=_lproj_rooted_path_or_basename, + resource_info=_resource_info, +) diff --git a/apple/bundling/rule_attributes.bzl b/apple/bundling/rule_attributes.bzl new file mode 100644 index 0000000000..af69f0a613 --- /dev/null +++ b/apple/bundling/rule_attributes.bzl @@ -0,0 +1,268 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines common attributes used in all Apple bundling rules. + +Note: The attributes for each rule are split into a few groupings: + +- Tool attributes: Private label-typed attributes that have default values + pointing to scripts and other common resources that are needed by all + target types. +- Public attributes: Attributes that the user specifies on their targets. +- Private non-tool attributes: Attributes with default values that differ + depending on the type of rule (application vs. extension, iOS vs. tvOS, + etc.). + +The last category of attributes in particular lets us achieve a sort of +"polymorphism" in our shared rule implementation functions, since they can +be effectively parameterized by grabbing rule-specific attributes from the +Skylark context. +""" + +load("//apple/bundling:apple_bundling_aspect.bzl", + "apple_bundling_aspect") +load("//apple:utils.bzl", "merge_dictionaries") + + +# Attributes that define tool dependencies. +def _tool_attributes(): + return { + "_actoolwrapper": attr.label( + cfg="host", + executable=True, + default=Label("@bazel_tools//tools/objc:actoolwrapper"), + ), + "_bundler_py": attr.label( + cfg="host", + single_file=True, + default=Label("//apple/bundling:bundler_py"), + ), + "_debug_entitlements": attr.label( + cfg="host", + allow_files=True, + single_file=True, + default=Label("@bazel_tools//tools/objc:device_debug_entitlements.plist"), + ), + "_dsym_info_plist_template": attr.label( + cfg="host", + single_file=True, + default=Label( + "//apple/bundling:dsym_info_plist_template"), + ), + "_environment_plist": attr.label( + cfg="host", + executable=True, + default=Label("@bazel_tools//tools/objc:environment_plist"), + ), + "_ibtoolwrapper": attr.label( + cfg="host", + executable=True, + default=Label("@bazel_tools//tools/objc:ibtoolwrapper"), + ), + "_ios_runner": attr.label( + cfg="host", + allow_files=True, + single_file=True, + default=Label("@bazel_tools//tools/objc:ios_runner.sh.mac_template"), + ), + "_momcwrapper": attr.label( + cfg="host", + executable=True, + default=Label("@bazel_tools//tools/objc:momcwrapper"), + ), + "_plisttool": attr.label( + cfg="host", + single_file=True, + default=Label("//apple/bundling:plisttool"), + ), + "_process_and_sign_template": attr.label( + single_file=True, + default=Label( + "//apple/bundling:process_and_sign_template"), + ), + "_realpath": attr.label( + cfg="host", + allow_files=True, + single_file=True, + default=Label("@bazel_tools//tools/objc:realpath"), + ), + "_std_redirect_dylib": attr.label( + cfg="host", + allow_files=True, + single_file=True, + default=Label("@bazel_tools//tools/objc:StdRedirect.dylib"), + ), + "_swiftstdlibtoolwrapper": attr.label( + cfg="host", + executable=True, + default=Label("@bazel_tools//tools/objc:swiftstdlibtoolwrapper"), + ), + "_xcrunwrapper": attr.label( + cfg="host", + executable=True, + default=Label("@bazel_tools//tools/objc:xcrunwrapper"), + ), + } + + +# To support the differing bundle directory structures between macOS and +# iOS/tvOS/watchOS, we use a set of format strings to determine where various +# types of files should go in the bundle. These format strings are: +# +# * `_path_in_archive_format`: The path relative to the archive root where the +# .app/.appex/etc. bundle should be placed. The placeholder "%s" is replaced +# with the name of the bundle. For example, an iOS application uses +# "Payload/%s" for this attribute, so an application named "Foo.app" will be +# placed in the final IPA archive at "Payload/foo.app". Extensions, which +# aren't shipped separately, just use "%s" to put them at the root of the ZIP +# archive. +# +# * `_bundle_contents_path_format`: The path relative to the bundle root where +# all of the bundle's contents should be placed; contents include the +# resources directory, binary directory, frameworks directory, plugins, code +# signature, Info.plist, and so forth. The placeholder "%s" is substituted by +# the destination path of a file relative to the bundle's contents. For +# example, iOS/tvOS/watchOS use simply "%s" as their contents path format, +# so a file like Info.plist is substituted in and stays the same; this path +# is then appended to the bundle root to yield ".../Foo.app/Info.plist". +# macOS apps have a Contents directory in their bundle root so they use +# "Contents/%s" as their contents path format, so Info.plist ends up in +# ".../Foo.app/Contents/Info.plist". +# +# * `_bundle_binary_path_format`: The path relative to the bundle's contents +# where the executable binary should be placed. iOS/tvOS/watchOS places +# this directly in the bundle's contents so they use simply "%s"; by +# combining this with the formats above, the path to Foo.app's binary is +# ".../Foo.app/Foo". macOS apps have a "MacOS" directory in their contents, +# so their binary path format is "MacOS/%s" and combined with above this +# yields ".../Foo.app/Contents/MacOS/Foo". +# +# * `_bundle_resources_path_format`: The path relative to the bundle's contents +# where resources should be placed. iOS/tvOS/watchOS places these directly in +# the bundle's contents so they use simply "%s"; by combining this with the +# formats above, the path to Foo.app's bar.strings file is +# ".../Foo.app/bar.strings". macOS apps have a "Resources" directory in their +# contents, so their resource path format is "Resources/%s" and combined with +# above this yields ".../Foo.app/Contents/Resources/bar.strings". +# +# To better visualize, iOS, tvOS, and watchOS bundles have the following +# structure, where the bundle, contents, binary, and resources paths are all +# the same: +# +# Payload/ +# Foo.app/ [bundle, contents, binary, and resources paths] +# Assets.car +# Foo (the binary) +# Info.plist +# OtherResource.strings +# PkgInfo +# PlugIns/ +# SomeExtension.appex/... +# +# On the other hand, macOS bundles have the following structure, where each of +# those paths differs: +# +# Foo.app/ [bundle path] +# Contents/ [contents path] +# MacOS/ [binary path] +# Foo (the binary) +# PlugIns/ +# SomeExtension.appex/... +# Resources/ [resources path] +# Assets.car +# OtherResource.strings +# +# Since the three modern Apple platforms use simpler bundle structures, those +# default values are provided here. The macOS rules override them with the +# appropriate values for that platform. + + +# Attributes that define the simpler iOS/tvOS/watchOS bundle directory +# structure. +def simple_path_format_attributes(): + return { + "_bundle_binary_path_format": attr.string(default="%s"), + "_bundle_contents_path_format": attr.string(default="%s"), + "_bundle_resources_path_format": attr.string(default="%s"), + } + + +# Attributes that define the special macOS bundle directory structure. +def macos_path_format_attributes(): + return { + "_bundle_binary_path_format": attr.string(default="MacOS/%s"), + "_bundle_contents_path_format": attr.string(default="Contents/%s"), + "_bundle_resources_path_format": attr.string(default="Resources/%s"), + } + + +# Attributes that are common to all packaging rules with or without +# user-provided binaries. +def common_rule_without_binary_attributes(): + return merge_dictionaries( + _tool_attributes(), + simple_path_format_attributes(), + { + "bundle_id": attr.string( + mandatory=True, + ), + "deps": attr.label_list( + aspects=[apple_bundling_aspect], + providers=[ + ["apple_resource"], + ["objc"], + ["swift"], + ], + ), + "infoplists": attr.label_list( + allow_files=[".plist"], + mandatory=True, + non_empty=True, + ), + # TODO(b/36512239): Rename to "archive_post_processor". + "ipa_post_processor": attr.label( + allow_files=True, + executable=True, + cfg="host", + ), + "provisioning_profile": attr.label( + allow_files=[".mobileprovision"], + single_file=True, + ), + "strings": attr.label_list(allow_files=[".strings"]), + # Whether or not the target should host a Frameworks directory or + # propagate its frameworks to the target in which it is embedded. For + # example, applications host frameworks (False, the default), but + # extensions have their frameworks bundled with the host application + # instead. + "_propagates_frameworks": attr.bool(default=False), + # Whether to skip all code signing. This is useful for artifacts that + # contain binaries but are meant for distribution to other developers + # to use in their own projects, where they will do their own signing + # and handle their own provisioning. + "_skip_signing": attr.bool(default=False), + } + ) + + +# Attributes that are common to all packaging rules with user-provided +# binaries. +def common_rule_attributes(): + return merge_dictionaries(common_rule_without_binary_attributes(), { + "binary": attr.label( + allow_rules=["apple_binary"], + aspects=[apple_bundling_aspect], + single_file=True, + ), + }) diff --git a/apple/bundling/run_actions.bzl b/apple/bundling/run_actions.bzl new file mode 100644 index 0000000000..53cd1f8b19 --- /dev/null +++ b/apple/bundling/run_actions.bzl @@ -0,0 +1,57 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common definitions used to make runnable Apple bundling rules.""" + +load("//apple:utils.bzl", "bash_quote") + + +def _start_simulator(ctx): + """Registers an action that runs the bundled app in the iOS simulator. + + This function requires that the calling rule include the `objc` configuration + fragment, outputs an `archive` with the IPA, and have the following tool + attributes: + + - `_std_redirect_dylib`: The StdRedirect.dylib file used to make an app's + output visible during the run. + + Args: + ctx: The Skylark context. + Returns: + A list of files that should be added to the calling rule's runfiles. + """ + ctx.template_action( + output=ctx.outputs.executable, + executable=True, + template=ctx.file._ios_runner, + substitutions={ + "%app_name%": ctx.label.name, + "%ipa_file%": ctx.outputs.archive.short_path, + "%sdk_version%": str(ctx.fragments.objc.ios_simulator_version), + "%sim_device%": bash_quote(ctx.fragments.objc.ios_simulator_device), + "%std_redirect_dylib_path%": ctx.file._std_redirect_dylib.short_path, + }, + ) + return [ + ctx.outputs.executable, + ctx.outputs.archive, + ctx.file._std_redirect_dylib, + ] + + +# Define the loadable module that lists the exported symbols in this file. +run_actions = struct( + start_simulator=_start_simulator, +) diff --git a/apple/bundling/swift_actions.bzl b/apple/bundling/swift_actions.bzl new file mode 100644 index 0000000000..bc0d4998af --- /dev/null +++ b/apple/bundling/swift_actions.bzl @@ -0,0 +1,68 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actions used to copy Swift libraries into the bundle.""" + +load("//apple/bundling:file_support.bzl", "file_support") +load("//apple/bundling:platform_support.bzl", + "platform_support") + + +def _zip_swift_dylibs(ctx): + """Registers an action that creates a ZIP that contains Swift dylibs. + + This action scans the binary associated with the target being built and + determines which Swift dynamic libraries need to be included in that + bundle. Some bundle types, like applications, will bundle them in their + Frameworks directory (as well as an archive-root SwiftSupport directory for + release builds); others, like extensions, will simply propagate them to the + host application. + + Args: + ctx: The Skylark context. + Returns: + A `File` object representing the ZIP file containing the Swift dylibs. + """ + platform, _ = platform_support.platform_and_sdk_version(ctx) + + xcrun_args = [] + if ctx.fragments.apple.xcode_toolchain: + xcrun_args += ["--toolchain", ctx.fragments.apple.xcode_toolchain] + + zip_file = file_support.intermediate(ctx, "%{name}.swiftlibs.zip") + platform_support.xcode_env_action( + ctx, + inputs=[ctx.file.binary], + outputs=[zip_file], + executable=ctx.executable._swiftstdlibtoolwrapper, + arguments=xcrun_args + [ + "--output_zip_path", + zip_file.path, + "--bundle_path", + ".", + "--platform", + platform.name_in_plist.lower(), + "--scan-executable", + ctx.file.binary.path, + ], + mnemonic="SwiftStdlibCopy", + no_sandbox=True, + ) + return zip_file + + +# Define the loadable module that lists the exported symbols in this file. +swift_actions = struct( + zip_swift_dylibs=_zip_swift_dylibs, +) diff --git a/apple/bundling/swift_support.bzl b/apple/bundling/swift_support.bzl new file mode 100644 index 0000000000..f84c387fef --- /dev/null +++ b/apple/bundling/swift_support.bzl @@ -0,0 +1,42 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions for working with Swift.""" + +load("//apple/bundling:provider_support.bzl", + "provider_support") + + +def _uses_swift(ctx): + """Returns True if the current target uses Swift. + + Note that this is not propagated through extensions or child apps (such as + Watch) -- that is, an Objective-C application that contains a Swift + application extension does not "use Swift" in the sense denoted by this + function. + + Args: + ctx: The Skylark context. + Returns: + True if the current target directly uses Swift; otherwise, False. + """ + swift_providers = provider_support.binary_or_deps_providers( + ctx, "AppleBundlingSwift") + return any([p.uses_swift for p in swift_providers]) + + +# Define the loadable module that lists the exported symbols in this file. +swift_support = struct( + uses_swift=_uses_swift, +) diff --git a/apple/bundling/test_support.bzl b/apple/bundling/test_support.bzl new file mode 100644 index 0000000000..722fc69028 --- /dev/null +++ b/apple/bundling/test_support.bzl @@ -0,0 +1,82 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support functions for testing Apple apps.""" + + +# List of objc provider fields that are whitelisted to be passed into the +# xctest_app provider's objc provider. This list needs to stay in sync with +# bazel's ReleaseBundlingSupport.java#xcTestAppProvider method. +_WHITELISTED_TEST_OBJC_PROVIDER_FIELDS = [ + "define", "dynamic_framework_file", "framework_dir", + "dynamic_framework_dir", "framework_search_paths", "header", "include", + "sdk_dylib", "sdk_framework", "source", "static_framework_file", + "weak_sdk_framework"] + + +# TODO(b/36513269): Remove xctest_app_provider once everyone has migrated out +# of native ios_application. This will be replaced by a test specific provider +# coming directly from apple_binary. +def _new_xctest_app_provider(ctx): + """Returns a newly configured xctest_app provider for the given context.""" + + test_objc_params = {} + for field in _WHITELISTED_TEST_OBJC_PROVIDER_FIELDS: + + if not hasattr(ctx.attr.binary.objc, field): + # Skip missing attributes from objc provider. This enables us to add + # fields yet to be released into the list of whitelisted fields that + # should be propagated by the xctest objc provider. + continue + + # This is safe to do because the list of fields is static and + # non-configurable, and because the non presence of a value is signaled by + # an empty set. Fields that do not yet exist are already filtered at this + # point. + field_value = depset(getattr(ctx.attr.binary.objc, field)) + if field_value: + destination_field = field + + # Filter out swift sdk_dylibs propagated, those are needed only if the + # tests depend on swift. If it doesn't propagating them will break as the + # linker will not be able to find them; the necessary linkopt comes from + # swift_library. If tests require swift dylibs, they will need to + # explicitly add them to their deps. + if field == "sdk_dylib": + field_value = depset([x for x in field_value + if not x.startswith("swift")]) + if not field_value: + # If there are no more values, prevent setting an empty set. + continue + + # In order to prevent double linking of frameworks in tests, we need to + # move the framework_dir paths into just search paths in order for tests + # to compile against the headers. + if field == "framework_dir" or field == "dynamic_framework_dir": + destination_field = "framework_search_paths" + + # Merge current values with the values from the binary's objc provider. + test_objc_params[destination_field] = test_objc_params.get( + destination_field, depset()) + field_value + + return apple_common.new_xctest_app_provider( + bundle_loader=ctx.file.binary, + ipa=ctx.outputs.archive, + objc_provider=apple_common.new_objc_provider(**test_objc_params)) + + +# Define the loadable module that lists the exported symbols in this file. +test_support = struct( + new_xctest_app_provider=_new_xctest_app_provider, +) diff --git a/apple/bundling/tvos_rules.bzl b/apple/bundling/tvos_rules.bzl new file mode 100644 index 0000000000..54b171a0c7 --- /dev/null +++ b/apple/bundling/tvos_rules.bzl @@ -0,0 +1,168 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bazel rules for creating tvOS applications and bundles. + +DO NOT load this file directly; use the macro in +//apple:tvos.bzl instead. Bazel rules receive their name at +*definition* time based on the name of the global to which they are assigned. +We want the user to call macros that have the same name, to get automatic +binary creation, entitlements support, and other features--which requires a +wrapping macro because rules cannot invoke other rules. +""" + +load("//apple/bundling:binary_support.bzl", "binary_support") +load("//apple/bundling:bundler.bzl", "bundler") +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:rule_attributes.bzl", + "common_rule_attributes") +load("//apple/bundling:run_actions.bzl", "run_actions") +load("//apple:utils.bzl", "merge_dictionaries") + + +def _tvos_application_impl(ctx): + """Implementation of the `tvos_application` Skylark rule.""" + + # Add the launch storyboard if we don't already have it in the set (which may + # happen if users glob "*.storyboard", for example). + additional_resources = depset() + launch_storyboard = ctx.file.launch_storyboard + if launch_storyboard: + additional_resources = depset([launch_storyboard]) + + additional_resources += ctx.files.app_icons + ctx.files.launch_images + + # If a settings bundle was provided, pass in its bundlable files (structs + # with a File object and destination path) to the core bundler, but only + # after transforming the paths to ensure that the files are copied into a + # directory named Settings.bundle. + additional_bundlable_files = [] + settings_bundle = ctx.attr.settings_bundle + if settings_bundle: + files = settings_bundle.objc.bundle_file + additional_bundlable_files = [ + bundling_support.force_settings_bundle_prefix(f) for f in files] + + # TODO(b/32910122): Obtain framework information from extensions. + embedded_bundles = [ + bundling_support.embedded_bundle( + "PlugIns", extension.apple_bundle, verify_bundle_id=True) + for extension in ctx.attr.extensions + ] + + providers, additional_outputs = bundler.run( + ctx, + "TvosExtensionArchive", "tvOS application", + ctx.attr.bundle_id, + additional_bundlable_files=additional_bundlable_files, + additional_resources=additional_resources, + embedded_bundles=embedded_bundles, + ) + runfiles = run_actions.start_simulator(ctx) + + # The empty tvos_application provider acts as a tag to let depending + # attributes restrict the targets that can be used to just tvOS applications. + return struct( + files=depset([ctx.outputs.archive]) + additional_outputs, + runfiles=ctx.runfiles(files=runfiles), + tvos_application=struct(), + **providers + ) + + +tvos_application = rule( + _tvos_application_impl, + attrs = merge_dictionaries(common_rule_attributes(), { + "app_icons": attr.label_list(), + "entitlements": attr.label( + allow_files=[".entitlements"], + single_file=True, + ), + "extensions": attr.label_list( + providers=[["apple_bundle", "tvos_extension"]], + ), + "launch_images": attr.label_list(), + "launch_storyboard": attr.label( + allow_files=[".storyboard", ".xib"], + single_file=True, + ), + "settings_bundle": attr.label(providers=[["objc"]]), + "_allowed_families": attr.string_list(default=["tv"]), + # The extension of the bundle being generated by the rule. + "_bundle_extension": attr.string(default=".app"), + # iOS .app bundles should include a PkgInfo file. + "_needs_pkginfo": attr.bool(default=True), + # A format string used to compose the path to the bundle inside the + # packaged archive. The placeholder "%s" is replaced with the name of + # the bundle (with its extension). + "_path_in_archive_format": attr.string(default="Payload/%s"), + # The platform type that should be passed to tools for targets of this + # type. + "_platform_type": attr.string( + default=str(apple_common.platform_type.tvos) + ), + }), + executable = True, + fragments = ["apple", "objc"], + outputs = { + "archive": "%{name}.ipa", + }, +) + + +def _tvos_extension_impl(ctx): + """Implementation of the `tvos_extension` Skylark rule.""" + providers, additional_outputs = bundler.run( + ctx, + "TvosExtensionArchive", "tvOS extension", + ctx.attr.bundle_id) + + # The empty tvos_extension provider acts as a tag to let depending attributes + # restrict the targets that can be used to just tvOS extensions. + return struct( + files=depset([ctx.outputs.archive]) + additional_outputs, + tvos_extension=struct(), + **providers + ) + + +tvos_extension = rule( + _tvos_extension_impl, + attrs = merge_dictionaries(common_rule_attributes(), { + "entitlements": attr.label( + allow_files=[".entitlements"], + single_file=True, + ), + "_allowed_families": attr.string_list(default=["tv"]), + # The extension of the bundle being generated by the rule. + "_bundle_extension": attr.string(default=".appex"), + # iOS extension bundles should not include a PkgInfo file. + "_needs_pkginfo": attr.bool(default=False), + # A format string used to compose the path to the bundle inside the + # packaged archive. The placeholder "%s" is replaced with the name of the + # bundle (with its extension). + "_path_in_archive_format": attr.string(default="%s"), + # The platform type that should be passed to tools for targets of this + # type. + "_platform_type": attr.string( + default=str(apple_common.platform_type.tvos) + ), + "_propagates_frameworks": attr.bool(default=True), + }), + fragments = ["apple", "objc"], + outputs = { + "archive": "%{name}.zip", + }, +) diff --git a/apple/bundling/watchos_rules.bzl b/apple/bundling/watchos_rules.bzl new file mode 100644 index 0000000000..2f5baaeeb8 --- /dev/null +++ b/apple/bundling/watchos_rules.bzl @@ -0,0 +1,163 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rule implementations for creating watchOS applications and bundles. + +DO NOT load this file directly; use the macro in +//apple:watchos.bzl instead. Bazel rules receive their name at +*definition* time based on the name of the global to which they are assigned. +We want the user to call macros that have the same name, to get automatic +binary creation, entitlements support, and other features--which requires a +wrapping macro because rules cannot invoke other rules. +""" + +load("//apple/bundling:bundler.bzl", "bundler") +load("//apple/bundling:bundling_support.bzl", + "bundling_support") +load("//apple/bundling:product_support.bzl", + "product_support") +load("//apple/bundling:rule_attributes.bzl", + "common_rule_attributes", + "common_rule_without_binary_attributes") +load("//apple/bundling:run_actions.bzl", "run_actions") +load("//apple:utils.bzl", "merge_dictionaries") + + +def _watchos_application_impl(ctx): + """Implementation of the watchos_application Skylark rule.""" + additional_resources = ctx.files.storyboards + ctx.files.app_icons + embedded_bundles = [] + + ext = ctx.attr.extension + if ext: + embedded_bundles.append(bundling_support.embedded_bundle( + "PlugIns", ext.apple_bundle, verify_bundle_id=True)) + + providers, additional_outputs = bundler.run( + ctx, + "WatchosApplicationArchive", "watchOS application", + ctx.attr.bundle_id, + additional_resources=additional_resources, + embedded_bundles=embedded_bundles, + ) + + # The empty watchos_application provider acts as a tag to let depending + # attributes restrict the targets that can be used to just watchOS + # applications. + # + # TODO(b/36513412): Support 'bazel run'. + return struct( + files=depset([ctx.outputs.archive]) + additional_outputs, + watchos_application=struct(), + **providers + ) + + +watchos_application = rule( + _watchos_application_impl, + attrs = merge_dictionaries(common_rule_without_binary_attributes(), { + "app_icons": attr.label_list(), + "entitlements": attr.label( + allow_files=[".entitlements"], + single_file=True, + ), + "extension": attr.label( + providers=[["apple_bundle", "watchos_extension"]], + mandatory=True, + ), + "storyboards": attr.label_list( + allow_files=[".storyboard"], + ), + "_allowed_families": attr.string_list(default=["watch"]), + # The extension of the bundle being generated by the rule. + "_bundle_extension": attr.string(default=".app"), + # iOS .app bundles should include a PkgInfo file. + "_needs_pkginfo": attr.bool(default=True), + # A format string used to compose the path to the bundle inside the + # packaged archive. The placeholder "%s" is replaced with the name of the + # bundle (with its extension). + "_path_in_archive_format": attr.string(default="%s"), + # The platform type that should be passed to tools for targets of this + # type. + "_platform_type": attr.string( + default=str(apple_common.platform_type.watchos) + ), + # The product type that should be passed to tools for targets of this + # type. + "_product_type": attr.string( + default=str(product_support.WATCHAPP2_PRODUCT_TYPE), + ), + }), + fragments = ["apple", "objc"], + outputs = { + "archive": "%{name}.zip", + }, +) + + +def _watchos_extension_impl(ctx): + """Implementation of the watchos_extension Skylark rule.""" + additional_resources = ctx.files.app_icons + + providers, additional_outputs = bundler.run( + ctx, + "WatchosExtensionArchive", "watchOS extension", + ctx.attr.bundle_id, + additional_resources=additional_resources, + ) + + # The empty watchos_extension provider acts as a tag to let depending + # attributes restrict the targets that can be used to just watchOS + # extensions. + return struct( + files=depset([ctx.outputs.archive]) + additional_outputs, + watchos_extension=struct(), + **providers + ) + + +watchos_extension = rule( + _watchos_extension_impl, + attrs = merge_dictionaries(common_rule_attributes(), { + "app_icons": attr.label_list(), + "entitlements": attr.label( + allow_files=[".entitlements"], + single_file=True, + ), + "_allowed_families": attr.string_list(default=["watch"]), + # The extension of the bundle being generated by the rule. + "_bundle_extension": attr.string(default=".appex"), + # iOS extension bundles should not include a PkgInfo file. + "_needs_pkginfo": attr.bool(default=False), + # A format string used to compose the path to the bundle inside the + # packaged archive. The placeholder "%s" is replaced with the name of the + # bundle (with its extension). + "_path_in_archive_format": attr.string(default="%s"), + # The platform type that should be passed to tools for targets of this + # type. + "_platform_type": attr.string( + default=str(apple_common.platform_type.watchos) + ), + # The product type that should be passed to tools for targets of this + # type. + "_product_type": attr.string( + default=str(product_support.WATCHKIT2_EXTENSION_PRODUCT_TYPE), + ), + "_propagates_frameworks": attr.bool(default=True), + }), + fragments = ["apple", "objc"], + outputs = { + "archive": "%{name}.zip", + }, +) diff --git a/apple/ios.bzl b/apple/ios.bzl new file mode 100644 index 0000000000..7590b5a193 --- /dev/null +++ b/apple/ios.bzl @@ -0,0 +1,250 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bazel rules for creating iOS applications and bundles.""" + +load("//apple/bundling:binary_support.bzl", "binary_support") + +# Alias the internal rules when we load them. This lets the rules keep their +# original name in queries and logs since they collide with the wrapper macros. +load("//apple/bundling:ios_rules.bzl", + _ios_application="ios_application", + _ios_extension="ios_extension", + _ios_framework="ios_framework", + ) + +# Explicitly export this because we want it visible to users loading this file. +load("//apple/bundling:product_support.bzl", + "apple_product_type") + + +def ios_application(name, **kwargs): + """Builds and bundles an iOS application. + + The named target produced by this macro is an IPA file. This macro also + creates a target named `"{name}.apple_binary"` that represents the linked + binary executable inside the application bundle. + + Args: + name: A unique name for the target. + app_icons: Files that comprise the app icons for the application. Each file + must have a containing directory named `*.xcassets/*.appiconset` and + there may be only one such `.appiconset` directory in the list. + bundle_id: The bundle ID (reverse-DNS path followed by app name) of the + application. + entitlements: The entitlements file required for device builds of this + application. If absent, the default entitlements from the provisioning + profile will be used. + + The following variables are substituted: `$(CFBundleIdentifier)` with + the bundle ID and `$(AppIdentifierPrefix)` with the value of the + `ApplicationIdentifierPrefix` key from this target's provisioning + profile. + extensions: A list of extensions (see `ios_extension`) to include in the + final application bundle. + families: A list of device families supported by this application. Valid + values are `iphone` and `ipad`; at least one must be specified. + frameworks: A list of framework targets (see `ios_framework`) that this + application depends on. + infoplists: A list of `.plist` files that will be merged to form the + Info.plist that represents the application. At least one file must be + specified. + ipa_post_processor: A tool that edits this target's IPA output after it is + assembled but before it is (optionally) signed. The tool is invoked with + a single command-line argument that denotes the path to a directory + containing the unzipped contents of the IPA (that is, the `Payload` + directory will be present in this directory). + + Any changes made by the tool must be made in this directory, and the + tool's execution must be hermetic given these inputs to ensure that the + result can be safely cached. + launch_images: Files that comprise the launch images for the application. + Each file must have a containing directory named + `*.xcassets/*.launchimage` and there may be only one such + `.launchimage` directory in the list. + + It is recommended that you use a `launch_storyboard` instead if you are + targeting only iOS 8 and later. + launch_storyboard: The `.storyboard` or `.xib` file that should be used as + the launch screen for the application. The provided file will be + compiled into the appropriate format (`.storyboardc` or `.nib`) and + placed in the root of the final bundle. The generated file will also be + registered in the bundle's `Info.plist` under the key + `UILaunchStoryboardName`. + linkopts: A list of strings representing extra flags that the underlying + `apple_binary` target created by this rule should pass to the linker. + product_type: An optional string denoting a special type of application, + such as a Messages Application in iOS 10 and higher. See + `apple_product_type`. + provisioning_profile: The provisioning profile (`.mobileprovision` file) to + use when bundling the application. This value is optional (and unused) + for simulator builds but **required** for device builds. + settings_bundle: An `objc_bundle` target that contains the files that make + up the application's settings bundle. These files will be copied into + the root of the final application bundle in a directory named + `Settings.bundle`. + strings: A list of `.strings` files, often localizable. + These files are converted to binary plists (if they are not already) + and placed in the root of the final application bundle, unless a file's + immediate containing directory is named `*.lproj`, in which case it will + be placed under a directory with the same name in the bundle. + watch_application: A `watchos_application` target that represents an Apple + Watch application that should be embedded in the application. + deps: A list of targets that are passed into the `apple_binary` rule to be + linked. Any resources, such as asset catalogs, that are referenced by + those targets will also be transitively included in the final + application. + + """ + bundling_args = binary_support.create_binary_if_necessary( + name, str(apple_common.platform_type.ios), **kwargs) + + _ios_application( + name = name, + **bundling_args + ) + + +def ios_extension(name, **kwargs): + """Builds and bundles an iOS application extension. + + The named target produced by this macro is a ZIP file. This macro also + creates a target named `"{name}.apple_binary"` that represents the linked + binary executable inside the extension bundle. + + Args: + name: The name of the target. + app_icons: Files that comprise the app icons for the extension. Each file + must have a containing directory named `"*.xcassets/*.appiconset"` and + there may be only one such `.appiconset` directory in the list. + bundle_id: The bundle ID (reverse-DNS path followed by app name) of the + extension. Required. + entitlements: The entitlements file required for device builds of this + application. If absent, the default entitlements from the provisioning + profile will be used. The following variables are substituted: + `$(CFBundleIdentifier)` with the bundle ID and `$(AppIdentifierPrefix)` + with the value of the `ApplicationIdentifierPrefix` key from this + target's provisioning profile (or the default provisioning profile, if + none is specified). + families: A list of device families supported by this extension. Valid + values are `"iphone"` and `"ipad"`. + frameworks: A list of framework targets (see `ios_framework`) that this + extension depends on. + infoplists: A list of `.plist` files that will be merged to form the + `Info.plist` that represents the extension. + ipa_post_processor: A tool that edits this target's archive after it is + assembled but before it is (optionally) signed. The tool is invoked + with a single positional argument that represents the path to a + directory containing the unzipped contents of the archive. The only + entry in this directory will be the `.appex` directory for the + extension. Any changes made by the tool must be made in this directory, + and the tool's execution must be hermetic given these inputs to ensure + that the result can be safely cached. + linkopts: A list of strings representing extra flags that the underlying + `apple_binary` target should pass to the linker. + product_type: An optional string denoting a special type of extension, + such as an iMessages sticker pack in iOS 10 and higher. + strings: A list of files that are plists of strings, often localizable. + These files are converted to binary plists (if they are not already) + and placed in the bundle root of the final package. If this file's + immediate containing directory is named `*.lproj`, it will be placed + under a directory of that name in the final bundle. This allows for + localizable strings. + deps: A list of dependencies, such as libraries, that are passed into the + `apple_binary` rule. Any resources, such as asset catalogs, that are + defined by these targets will also be transitively included in the + final extension. + """ + + # Add extension-specific linker options. Note that since apple_binary + # prepends "-Wl," to each option, we must use the form expected by ld, not + # the form expected by clang (i.e., -application_extension, not + # -fapplication-extension). + linkopts = kwargs.get("linkopts", []) + linkopts += ["-e", "_NSExtensionMain", "-application_extension"] + kwargs["linkopts"] = linkopts + + bundling_args = binary_support.create_binary_if_necessary( + name, str(apple_common.platform_type.ios), **kwargs) + + _ios_extension( + name = name, + **bundling_args + ) + + +def ios_framework(name, **kwargs): + """Builds and bundles an iOS dynamic framework. + + The named target produced by this macro is a ZIP file. This macro also + creates a target named "{name}.apple_binary" that represents the + linked binary executable inside the framework bundle. + + Args: + name: The name of the target. + bundle_id: The bundle ID (reverse-DNS path followed by app name) of the + framework. If specified, it will override the bundle ID in the plist + file. If no bundle ID is specified by either this attribute or in the + plist file, the build will fail. + families: A list of device families supported by this framework. Valid + values are `"iphone"` and `"ipad"`. + infoplists: A list of `.plist` files that will be merged to form the + Info.plist that represents the framework. + ipa_post_processor: A tool that edits this target's archive after it is + assembled but before it is (optionally) signed. The tool is invoked + with a single positional argument that represents the path to a + directory containing the unzipped contents of the archive. The only + entry in this directory will be the `.framework` directory for the + framework. Any changes made by the tool must be made in this directory, + and the tool's execution must be hermetic given these inputs to ensure + that the result can be safely cached. + linkopts: A list of strings representing extra flags that the underlying + `apple_binary` target should pass to the linker. + strings: A list of files that are plists of strings, often localizable. + These files are converted to binary plists (if they are not already) + and placed in the bundle root of the final package. If this file's + immediate containing directory is named `*.lproj`, it will be placed + under a directory of that name in the final bundle. This allows for + localizable strings. + deps: A list of dependencies, such as libraries, that are passed into the + `apple_binary` rule. Any resources, such as asset catalogs, that are + defined by these targets will also be transitively included in the + final framework. + """ + deps = kwargs.get("deps") + apple_dylib_name = "%s.apple_binary" % name + + linkopts = kwargs.get("linkopts", []) + linkopts += ["-install_name", "@rpath/%s.framework/%s" % (name, name)] + + # Link the executable from any library deps and sources provided. + native.apple_binary( + name = apple_dylib_name, + binary_type = "dylib", + hdrs = kwargs.get("hdrs", []), + platform_type = str(apple_common.platform_type.ios), + deps = deps, + testonly = kwargs.get("testonly"), + linkopts = linkopts, + ) + + # Remove any kwargs that shouldn't be passed to the underlying rule. + passthrough_args = kwargs + passthrough_args.pop("entitlements", None) + + _ios_framework( + name = name, + binary = apple_dylib_name, + **passthrough_args + ) diff --git a/apple/providers.bzl b/apple/providers.bzl new file mode 100644 index 0000000000..10f83699ff --- /dev/null +++ b/apple/providers.bzl @@ -0,0 +1,204 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines providers and related types used throughout the bundling rules. + +These providers are part of the public API of the bundling rules. Other rules +that want to propagate information to the bundling rules or that want to +consume the bundling rules as their own inputs should use these to handle the +relevant information that they need. +""" + +# TODO(b/34879141): Convert these to declared providers. +def AppleBundlingSwift(uses_swift=False): + """Returns a new `AppleBundlingSwift` provider. + + The `AppleBundlingSwift` provider is used to indicate whether Swift is + required by any code in the bundle. Note that this only applies within + the bundle's direct dependencies (`deps`); it does not pass through + application/extension boundaries. For example, if an extension uses + Swift but an application does not, then the application does not "use + Swift" as defined by this provider. + + Args: + uses_swift: True if Swift is used by the target propagating this + provider or by any of its transitive dependencies. + Returns: + A new `AppleBundlingSwift` provider. + """ + return struct(uses_swift=uses_swift) + + +def AppleResource(resource_sets=[]): + """Returns a new `AppleResource` provider. + + The `AppleResource` provider should be propagated by rules that want to + propagate resources--such as images, strings, Interface Builder files, and so + forth--to a depending application or extension. For example, `swift_library` + can provide attributes like `bundles`, `resources`, and + `structured_resources` that allow users to associate resources with the code + that uses them. + + Args: + resource_sets: A list of structs (each returned by `AppleResourceSet`) + that describe the transitive resources propagated by this rule. + Returns: + A new `AppleResource` provider. + """ + return struct(resource_sets=resource_sets) + + +def AppleResourceSet(bundle_dir=None, + infoplists=depset(), + objc_bundle_imports=depset(), + resources=depset(), + structured_resources=depset(), + structured_resource_zips=depset(), + swift_module=None): + """Returns a new resource set to be propagated via `apple_resource`. + + Args: + bundle_dir: The path within the final bundle (relative to its resources + root) where the resources should be stored. For example, a resource + bundle rule would specify something of the form `"Foo.bundle"` here; + library rules that propagate resources to the application itself + should specify `None` (or omit it, as `None` is the default). + infoplists: A `depset` of `File`s representing plists that should be + merged to produce the `Info.plist` for the bundle. + objc_bundle_imports: A `depset` of `File`s representing resources that + came from an `objc_bundle` target and need to have their paths stripped + of any segments before the `"*.bundle"` name. + resources: A `depset` of `File`s representing resources that should be + processed (if they are a known type) or copied (if the type is not + recognized) and placed in the bundle at the location specified by + `bundle_dir`. The relative paths to these files are ignored, with the + exception that files contained in a directory named `"*.lproj"` will + be placed in a directory of the same name in the final bundle. + structured_resources: A `depset` of `File`s representing resources that + should be copied into the bundle without any processing at the location + specified by `bundle_dir`. The relative paths of these files are + preserved. + structured_resource_zips: A `depset` of `File`s representing ZIP archives + whose contents should unzipped into the bundle without any processing + at the location specified by `bundle_dir`. The directory structure + within the archive is preserved. + swift_module: The name of the Swift module with which these resources are + associated. Some resource types, such as Interface Builder files or + Core Data models, require the Swift module to be specified during + compilation so that the classes they reference can be found at runtime. + If this value is `None`, then the resources are not associated with a + Swift module (for example, resources attached to Objective-C rules) and + the name of the main application/extension/framework will be passed to + the resource tool instead. + Returns: + A struct containing a set of resources that can be propagated by the + `apple_resource` provider. + """ + return struct(bundle_dir=bundle_dir, + infoplists=infoplists, + objc_bundle_imports=objc_bundle_imports, + resources=resources, + structured_resources=structured_resources, + structured_resource_zips=structured_resource_zips, + swift_module=swift_module) + + +def _apple_resource_set_utils_minimize(resource_sets): + """Minimizes a list of resource sets by merging similar elements. + + Two or more resource sets can be merged if their `bundle_dir` and + `swift_module` values are the same, which means that they can be passed to + the same resource processing tool invocation. The list returned by this + function represents the minimal possible list after merging such sets. + + The main Apple bundler will minimize the list of transitive resource sets + before processing resources, but other rules that propagate resource sets are + advised to call this function as well after collecting their transitive + resources to avoid propagating a large number of minimizable sets to their + dependers. + + Args: + resource_sets: The list of `AppleResourceSet` values that should be merged. + Returns: + The minimal possible list after merging `AppleResourceSet` values with + the same `bundle_dir` and `swift_module`. + """ + minimized_dict = {} + + for current_set in resource_sets: + key = (current_set.bundle_dir, current_set.swift_module) + existing_set = minimized_dict.get(key) + + if existing_set: + new_set = AppleResourceSet( + bundle_dir=existing_set.bundle_dir, + infoplists=existing_set.infoplists + current_set.infoplists, + objc_bundle_imports=(existing_set.objc_bundle_imports + + current_set.objc_bundle_imports), + resources=existing_set.resources + current_set.resources, + structured_resources=(existing_set.structured_resources + + current_set.structured_resources), + structured_resource_zips=(existing_set.structured_resource_zips + + current_set.structured_resource_zips), + swift_module=existing_set.swift_module, + ) + else: + new_set = current_set + + minimized_dict[key] = new_set + + return minimized_dict.values() + + +def _apple_resource_set_utils_prefix_bundle_dir(resource_set, prefix): + """Returns an equivalent resource set with a new path prepended to it. + + This function should be used by rules that allow nested bundles; for example, + a resource bundle that contains other resource bundles must prepend its own + `bundle_dir` to the `bundle_dir`s of its child bundles to ensure that the + files are bundled in the correct location. + + For example, if `resource_set` has a `bundle_dir` of `"Foo.bundle"` and + `prefix` is `"Bar.bundle"`, the returned resource set will have a + `bundle_dir` equal to `"Bar.bundle/Foo.bundle"`. Likewise, if `resource_set` + had a `bundle_dir` of `None`, then the new `bundle_dir` would be + `"Bar.bundle"`. + + Args: + resource_set: The `AppleResourceSet` whose `bundle_dir` should be prefixed. + prefix: The path that should be prepended to the existing `bundle_dir`. + Returns: + A new `AppleResourceSet` whose `bundle_dir` has been prefixed with the + given path. + """ + nested_dir = prefix + if resource_set.bundle_dir: + nested_dir += "/" + resource_set.bundle_dir + + return AppleResourceSet( + bundle_dir=nested_dir, + infoplists=resource_set.infoplists, + objc_bundle_imports=resource_set.objc_bundle_imports, + resources=resource_set.resources, + structured_resources=resource_set.structured_resources, + structured_resource_zips=resource_set.structured_resource_zips, + swift_module=resource_set.swift_module, + ) + + +# Export the module containing helper functions for resource sets. +apple_resource_set_utils = struct( + minimize=_apple_resource_set_utils_minimize, + prefix_bundle_dir=_apple_resource_set_utils_prefix_bundle_dir, +) diff --git a/apple/resources.bzl b/apple/resources.bzl new file mode 100644 index 0000000000..2032e74267 --- /dev/null +++ b/apple/resources.bzl @@ -0,0 +1,28 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rules related to Apple resources and resource bundles.""" + + +def objc_bundle_library(**kwargs): + """Creates an `objc_bundle_library` target. + + This rule is a placeholder that will be updated after everyone has migrated + to the Skylark rules and the native rule has been deleted. + + Args: + **kwargs: Arguments that will be passed directly into the native + `objc_bundle_library` rule. + """ + native.objc_bundle_library(**kwargs) diff --git a/apple/tvos.bzl b/apple/tvos.bzl new file mode 100644 index 0000000000..90bf16c3eb --- /dev/null +++ b/apple/tvos.bzl @@ -0,0 +1,164 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bazel rules for creating tvOS applications and bundles.""" + +load("//apple/bundling:binary_support.bzl", "binary_support") + +# Alias the internal rules when we load them. This lets the rules keep their +# original name in queries and logs since they collide with the wrapper macros. +load("//apple/bundling:tvos_rules.bzl", + _tvos_application="tvos_application", + _tvos_extension="tvos_extension", + ) + + +def tvos_application(name, **kwargs): + """Builds and bundles a tvOS application. + + The named target produced by this macro is an IPA file. This macro also + creates a target named `"{name}.apple_binary"` that represents the linked + binary executable inside the application bundle. + + Args: + name: The name of the target. + app_icons: Files that comprise the app icons for the application. Each file + must have a containing directory named `"*.xcassets/*.appiconset"` and + there may be only one such `.appiconset` directory in the list. + bundle_id: The bundle ID (reverse-DNS path followed by app name) of the + application. If specified, it will override the bundle ID in the plist + file. If no bundle ID is specified by either this attribute or in the + plist file, the build will fail. + entitlements: The entitlements file required for device builds of this + application. If absent, the default entitlements from the provisioning + profile will be used. The following variables are substituted: + `$(CFBundleIdentifier)` with the bundle ID and `$(AppIdentifierPrefix)` + with the value of the `ApplicationIdentifierPrefix` key from this + target's provisioning profile (or the default provisioning profile, if + none is specified). + extensions: A list of extensions (see `tvos_extension`) to include in the + final application. + infoplists: A list of `.plist` files that will be merged to form the + Info.plist that represents the application. + ipa_post_processor: A tool that edits this target's IPA output after it is + assembled but before it is (optionally) signed. The tool is invoked + with a single positional argument that represents the path to a + directory containing the unzipped contents of the IPA. The only entry + in this directory will be the Payload root directory of the IPA. Any + changes made by the tool must be made in this directory, and the tool's + execution must be hermetic given these inputs to ensure that the result + can be safely cached. + launch_images: Files that comprise the launch images for the application. + Each file must have a containing directory named + `"*.xcassets/*.launchimage"` and there may be only one such + `.launchimage` directory in the list. + launch_storyboard: The `.storyboard` or `.xib` file that should be used as + the launch screen for the application. The provided file will be + compiled into the appropriate format and placed in the root of the + final bundle. The generated file is registered in the final bundle's + `Info.plist` under the key `UILaunchStoryboardName`. + linkopts: A list of strings representing extra flags that the underlying + `apple_binary` target should pass to the linker. + provisioning_profile: The provisioning profile (`.mobileprovision` file) to + use when bundling the application. This is only used for non-simulator + builds. + settings_bundle: An `objc_bundle` target that contains the files that make + up the application's settings bundle. These files will be copied into + the application in a directory named `Settings.bundle`. + strings: A list of files that are plists of strings, often localizable. + These files are converted to binary plists (if they are not already) + and placed in the bundle root of the final package. If this file's + immediate containing directory is named `*.lproj`, it will be placed + under a directory of that name in the final bundle. This allows for + localizable strings. + deps: A list of dependencies, such as libraries, that are passed into the + `apple_binary` rule. Any resources, such as asset catalogs, that are + defined by these targets will also be transitively included in the + final application. + """ + bundling_args = binary_support.create_binary_if_necessary( + name, str(apple_common.platform_type.tvos), **kwargs) + + _tvos_application( + name = name, + **bundling_args + ) + + +def tvos_extension(name, **kwargs): + """Builds and bundles a tvOS extension. + + The named target produced by this macro is a ZIP file. This macro also + creates a target named `"{name}.apple_binary"` that represents the linked + binary executable inside the extension bundle. + + Args: + name: The name of the target. + bundle_id: The bundle ID (reverse-DNS path followed by app name) of the + extension. If specified, it will override the bundle ID in the plist + file. If no bundle ID is specified by either this attribute or in the + plist file, the build will fail. + entitlements: The entitlements file required for device builds of this + application. If absent, the default entitlements from the provisioning + profile will be used. The following variables are substituted: + `$(CFBundleIdentifier)` with the bundle ID and `$(AppIdentifierPrefix)` + with the value of the `ApplicationIdentifierPrefix` key from this + target's provisioning profile (or the default provisioning profile, if + none is specified). + infoplists: A list of `.plist` files that will be merged to form the + `Info.plist` that represents the extension. + ipa_post_processor: A tool that edits this target's archive after it is + assembled but before it is (optionally) signed. The tool is invoked + with a single positional argument that represents the path to a + directory containing the unzipped contents of the archive. The only + entry in this directory will be the `.appex` directory for the + extension. Any changes made by the tool must be made in this + directory, and the tool's execution must be hermetic given these inputs + to ensure that the result can be safely cached. + linkopts: A list of strings representing extra flags that the underlying + `apple_binary` target should pass to the linker. + strings: A list of files that are plists of strings, often localizable. + These files are converted to binary plists (if they are not already) + and placed in the bundle root of the final package. If this file's + immediate containing directory is named `*.lproj`, it will be placed + under a directory of that name in the final bundle. This allows for + localizable strings. + deps: A list of dependencies, such as libraries, that are passed into the + `apple_binary` rule. Any resources, such as asset catalogs, that are + defined by these targets will also be transitively included in the + final extension. + """ + + # Add extension-specific linker options. Note that since apple_binary + # prepends "-Wl," to each option, we must use the form expected by ld, not + # the form expected by clang (i.e., -application_extension, not + # -fapplication-extension). + linkopts = kwargs.get("linkopts", []) + linkopts += ["-e", "_TVExtensionMain", "-application_extension"] + kwargs["linkopts"] = linkopts + + # Make sure that TVServices.framework is linked in as well, to ensure that + # _TVExtensionMain is found. (Anyone writing a TV extension should already be + # importing this framework, anyway.) + bundling_args = binary_support.create_binary_if_necessary( + name, + str(apple_common.platform_type.tvos), + sdk_frameworks=["TVServices"], + **kwargs + ) + + _tvos_extension( + name = name, + **bundling_args + ) diff --git a/apple/utils.bzl b/apple/utils.bzl new file mode 100644 index 0000000000..72531413b1 --- /dev/null +++ b/apple/utils.bzl @@ -0,0 +1,373 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for working with strings, lists, and files in Skylark.""" + + +def apple_action(ctx, **kw): + """Creates an action that only runs on MacOS/Darwin. + + Call it similar to how you would call ctx.action: + apple_action(ctx, outputs=[...], inputs=[...],...) + """ + execution_requirements = kw.get("execution_requirements", {}) + execution_requirements["requires-darwin"] = "" + + no_sandbox = kw.pop("no_sandbox", False) + if no_sandbox: + execution_requirements["nosandbox"] = "1" + + kw["execution_requirements"] = execution_requirements + ctx.action(**kw) + + +def basename(path): + """Returns the basename (i.e., the file portion) of a path. + + Args: + path: The path whose basename should be returned. + Returns: + The basename of the path, which includes the extension. + """ + return path.rpartition('/')[-1] + + +def bash_array_string(iterable): + """Creates a string from a sequence that can be used as a Bash array. + + Args: + iterable: A sequence of elements. + Returns: + A string that represents the sequence as a Bash array; that is, parentheses + containing the elements surrounded by double-quotes. + """ + return '(' + ' '.join([bash_quote(i) for i in iterable]) + ')' + + +def bash_quote(s): + """Returns a quoted representation of the given string for Bash. + + This function double-quotes the given string (in case it contains spaces or + other special characters) and escapes any dollar signs or double-quotes that + might already be inside it. + + Args: + s: The string to quote. + Returns: + An escaped and quoted version of the string that can be passed to a command + in a Bash script. + """ + return '"' + s.replace('$', '\\$').replace('"', '\\"') + '"' + + +def dirname(path): + """Returns the dirname (i.e., everything but the file portion) of a path. + + Args: + path: The path whose dirname should be returned. + Returns: + The dirname of the path. + """ + return path.rpartition('/')[0] + + +def full_label(l): + """Converts a label to full format, e.g. //a/b/c -> //a/b/c:c. + + If the label is already in full format, it returns it as it is, otherwise + appends the folder name as the target name. + + Args: + l: The label to convert to full format. + Returns: + The label in full format, or the original input if it was already in full + format. + """ + if l.find(':') != -1: + return l + target_name = l.rpartition('/')[-1] + return l + ':' + target_name + + +def group_files_by_directory(files, extensions, attr): + """Groups files based on their containing directories. + + This function examines each file in |files| and looks for a containing + directory with the given extension. It then returns a dictionary that maps + the directory names to the files they contain. + + For example, if you had the following files: + - some/path/foo.images/bar.png + - some/path/foo.images/baz.png + - some/path/quux.images/blorp.png + + Then passing the extension "images" to this function would return: + { + "some/path/foo.images": depset([ + "some/path/foo.images/bar.png", + "some/path/foo.images/baz.png" + ]), + "some/path/quux.images": depset([ + "some/path/quux.images/blorp.png" + ]) + } + + Note that |files| can be an iterable of either strings (paths) or File + objects, and the returned dictionary preserves these input values. In other + words, if it contains strings, then the sets in the dictionary will also + contain strings (retaining the directory path as well). If the input elements + are File objects, the returned dictionary values will also be sets of those + File objects. + + If an input file does not have a containing directory with the given + extension, the build will fail. + + Args: + files: An iterable of File objects or strings representing paths. + extensions: The list of extensions of the containing directories to return. + The extensions should NOT include the leading dot. + attr: The attribute to associate with the build failure if the list of + files has an element that is not in a directory with the given + extension. + Returns: + A dictionary whose keys are directories with the given extension and their + values are the sets of files within them. + """ + grouped_files = {} + files_that_matched = depset() + + for extension in extensions: + search_string = '.%s' % extension + + for f in files: + if type(f) == type(''): + path = f + else: + path = f.path + + # Make sure the matched string either has a '/' after it, or occurs at + # the end of the string (this lets us match directories without requiring + # a trailing slash but prevents matching something like '.xcdatamodeld' + # when passing 'xcdatamodel'). The ordering of these checks is also + # important, to ensure that we can handle cases that occur when working + # with common Apple file structures, like passing 'xcdatamodel' and + # correctly parsing paths matching 'foo.xcdatamodeld/bar.xcdatamodel/...'. + after_index = -1 + search_len = len(search_string) + index_with_slash = path.find(search_string + '/') + if index_with_slash != -1: + after_index = index_with_slash + search_len + else: + index_without_slash = path.find(search_string) + after_index = index_without_slash + search_len + # If the search string wasn't at the end of the string, it must have a + # non-slash character after it (because we already checked the slash case + # above), so eliminate it. + if after_index != len(path): + after_index = -1 + + if after_index != -1: + files_that_matched += [f] + container = path[:after_index] + if container in grouped_files: + grouped_files[container] += [f] + else: + grouped_files[container] = depset([f]) + + if len(files_that_matched) < len(files): + unmatched_files = [f.path for f in files if f not in files_that_matched] + formatted_files = '[\n %s\n]' % ',\n '.join(unmatched_files) + fail('Expected only files inside directories named with the extensions ' + + '%r, but found: %s' % (extensions, formatted_files), attr) + + return grouped_files + + +def intersperse(separator, iterable): + """Inserts separator before each item in iterable. + + Args: + separator: The value to insert before each item in iterable. + iterable: The list into which to intersperse the separator. + Returns: + A new list with separator before each item in iterable. + """ + result = [] + for x in iterable: + result.append(separator) + result.append(x) + + return result + + +def join_commands(cmds): + """Joins a list of shell commands with ' && '. + + Args: + cmds: The list of commands to join. + Returns: + A string with the given commands joined with ' && ', suitable for use in a + shell script action. + """ + return ' && '.join(cmds) + + +def merge_dictionaries(*dictionaries): + """Merges at least two dictionaries. + + If any of the dictionaries share keys, the result will contain the value from + the latest one in the list. + + Args: + *dictionaries: The dictionaries that should be merged. + Returns: + The dictionary with all the attributes. + """ + result = {} + for d in dictionaries: + for name, value in d.items(): + result[name] = value + return result + + +def optionally_prefixed_path(path, prefix): + """Returns a path with an optional prefix. + + The prefix will be treated as an ancestor directory, so for example: + + ``` + optionally_prefixed_path("foo", None) == "foo" + optionally_prefixed_path("foo", "bar") == "bar/foo" + ``` + + Args: + path: The path. + prefix: If None or empty, `path` will be returned; otherwise, the prefix + will be treated as an ancestor directory and will be prepended to the + path, with a slash. + Returns: + The path, optionally prepended with the prefix. + """ + if prefix: + return prefix + "/" + path + return path + + +def relativize_path(path, ancestor): + """Returns the portion of `path` that is relative to `ancestor`. + + This function does not normalize paths (for example, it does not handle + segments that are ".." or "."), so it should not be used in contexts where + those segments might exist. It will fail the build if `path` is not beneath + `ancestor`. + + Args: + path: The path to relativize. + ancestor: The ancestor path against which to relativize. + Returns: + The portion of `path` that is relative to `ancestor`. + """ + segments = [s for s in path.split('/') if s] + ancestor_segments = [s for s in ancestor.split('/') if s] + ancestor_length = len(ancestor_segments) + + if (path.startswith('/') != ancestor.startswith('/') or + len(segments) < ancestor_length): + fail('Path %r is not beneath %r' % (path, ancestor)) + + for ancestor_segment, segment in zip(ancestor_segments, segments): + if ancestor_segment != segment: + fail('Path %r is not beneath %r' % (path, ancestor)) + + length = len(segments) - ancestor_length + result_segments = segments[-length:] + return '/'.join(result_segments) + + +def remove_extension(filename): + """Removes the extension from a file. + + The filename is returned unchanged if the basename does not have an + extension. + + Args: + filename: The filename whose extension should be removed. + Returns: + The filename with the extension removed, or the same filename if it did not + have an extension. + """ + last_dot = filename.rfind('.') + if last_dot == -1: + return filename + last_slash = filename.rfind('/') + if last_slash > last_dot: + return filename + return filename[:last_dot] + + +def replace_extension(filename, new_extension): + """Replaces the extension of the file at the end of a path. + + If the file has no extension, the new extension is added to it. + + Args: + filename: The filename whose extension should be replaced. + new_extension: The new extension for the file. The new extension should + begin with a dot if you want the new filename to have one. + Returns: + The path with the extension replaced (or added, if it did not have one). + """ + return remove_extension(filename) + new_extension + + +def split_extension(filename): + """Splits the name of a file from its extension and returns both. + + Args: + filename: The filename that should be split. + Returns: + A tuple containing two values: the name of the file without the extension, + and the extension (with the leading dot). If the file had no extension, + then the second element of the tuple is the empty string. + """ + last_dot = filename.rfind('.') + if last_dot == -1: + return (filename, '') + last_slash = filename.rfind('/') + if last_slash > last_dot: + return (filename, '') + return (filename[:last_dot], filename[last_dot:]) + + +def xcrun_env(ctx): + """Returns the environment dictionary necessary to use xcrunwrapper.""" + platform = ctx.fragments.apple.single_arch_platform + action_env = (ctx.fragments.apple.target_apple_env(platform) + + ctx.fragments.apple.apple_host_system_env()) + return action_env + + +def xcrun_action(ctx, **kw): + """Creates an apple action that executes xcrunwrapper. + + args: + ctx: The context of the rule that owns this action. + + This method takes the same keyword arguments as ctx.action, however you don't + need to specify the executable. + """ + env = kw.get("env", {}) + kw["env"] = env + xcrun_env(ctx) + + apple_action(ctx, executable=ctx.executable._xcrunwrapper, **kw) diff --git a/apple/watchos.bzl b/apple/watchos.bzl new file mode 100644 index 0000000000..3745fe5610 --- /dev/null +++ b/apple/watchos.bzl @@ -0,0 +1,164 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bazel rules for creating watchOS applications and bundles.""" + +load("//apple/bundling:binary_support.bzl", "binary_support") +load("//apple/bundling:product_support.bzl", "product_support") + +# Alias the internal rules when we load them. This lets the rules keep their +# original name in queries and logs since they collide with the wrapper macros. +load("//apple/bundling:watchos_rules.bzl", + _watchos_application="watchos_application", + _watchos_extension="watchos_extension", + ) + + +def watchos_application(name, **kwargs): + """Builds and bundles a watchOS application. + + This rule only supports watchOS 2.0 and higher. It cannot be used to produce + watchOS 1.x application, as Apple no longer supports that version of the + platform. + + The named target produced by this macro is a zip file. The watch application + is not executable or installable by itself; it must be used by adding the + target to a companion `"ios_application"` using the `"watch_application"` + attribute on that rule. + + Args: + name: The name of the target. + app_icons: Files that comprise the app icons for the application. Each file + must have a containing directory named `"*.xcassets/*.appiconset"` and + there may be only one such `.appiconset` directory in the list. + bundle_id: The bundle ID (reverse-DNS path followed by app name) of the + application. If specified, it will override the bundle ID in the plist + file. If no bundle ID is specified by either this attribute or in the + plist file, the build will fail. + entitlements: The entitlements file required for device builds of this + application. If absent, the default entitlements from the provisioning + profile will be used. The following variables are substituted: + `$(CFBundleIdentifier)` with the bundle ID and `$(AppIdentifierPrefix)` + with the value of the `ApplicationIdentifierPrefix` key from this + target's provisioning profile (or the default provisioning profile, if + none is specified). + extension: The watch extension (see `watchos_extension`) to bundle with + this application. This attribute is required. + infoplists: A list of `.plist` files that will be merged to form the + Info.plist that represents the application. + ipa_post_processor: A tool that edits this target's IPA output after it is + assembled but before it is (optionally) signed. The tool is invoked + with a single positional argument that represents the path to a + directory containing the unzipped contents of the IPA. The only entry + in this directory will be the Payload root directory of the IPA. Any + changes made by the tool must be made in this directory, and the tool's + execution must be hermetic given these inputs to ensure that the result + can be safely cached. + provisioning_profile: The provisioning profile (`.mobileprovision` file) to + use when bundling the application. This is only used for non-simulator + builds. + strings: A list of files that are plists of strings, often localizable. + These files are converted to binary plists (if they are not already) + and placed in the bundle root of the final package. If this file's + immediate containing directory is named `*.lproj`, it will be placed + under a directory of that name in the final bundle. This allows for + localizable strings. + deps: A list of dependencies whose resources will be included in the final + application bundle. (Since a watchOS application does not contain any + code of its own, any code in the dependent libraries will be ignored.) + """ + + # The macro below can't see the default argument in the attribute, so we have + # to duplicate it here. + kwargs["_product_type"] = product_support.WATCHAPP2_PRODUCT_TYPE + + bundling_args = binary_support.create_binary_if_necessary( + name, str(apple_common.platform_type.watchos), **kwargs) + + _watchos_application( + name = name, + **bundling_args + ) + + +def watchos_extension(name, **kwargs): + """Builds and bundles a watchOS extension. + + This rule only supports watchOS 2.0 and higher. It cannot be used to produce + watchOS 1.x application, as Apple no longer supports that version of the + platform. + + The named target produced by this macro is a ZIP file. This macro also + creates a target named `"{name}.apple_binary"` that represents the linked + binary executable inside the extension bundle. + + Args: + name: The name of the target. + app_icons: Files that comprise the app icons for the extension. Each file + must have a containing directory named `"*.xcassets/*.appiconset"` and + there may be only one such .appiconset directory in the list. + bundle_id: The bundle ID (reverse-DNS path followed by app name) of the + extension. If specified, it will override the bundle ID in the plist + file. If no bundle ID is specified by either this attribute or in the + plist file, the build will fail. + entitlements: The entitlements file required for device builds of this + application. If absent, the default entitlements from the provisioning + profile will be used. The following variables are substituted: + `$(CFBundleIdentifier)` with the bundle ID and `$(AppIdentifierPrefix)` + with the value of the `ApplicationIdentifierPrefix` key from this + target's provisioning profile (or the default provisioning profile, if + none is specified). + infoplists: A list of `.plist` files that will be merged to form the + Info.plist that represents the extension. + ipa_post_processor: A tool that edits this target's archive after it is + assembled but before it is (optionally) signed. The tool is invoked + with a single positional argument that represents the path to a + directory containing the unzipped contents of the archive. The only + entry in this directory will be the `.appex` directory for the + extension. Any changes made by the tool must be made in this directory, + and the tool's execution must be hermetic given these inputs to ensure + that the result can be safely cached. + linkopts: A list of strings representing extra flags that the underlying + `apple_binary` target should pass to the linker. + strings: A list of files that are plists of strings, often localizable. + These files are converted to binary plists (if they are not already) + and placed in the bundle root of the final package. If this file's + immediate containing directory is named `*.lproj`, it will be placed + under a directory of that name in the final bundle. This allows for + localizable strings. + deps: A list of dependencies, such as libraries, that are passed into the + `apple_binary` rule. Any resources, such as asset catalogs, that are + defined by these targets will also be transitively included in the + final extension. + """ + + # Add extension-specific linker options. Note that since apple_binary + # prepends "-Wl," to each option, we must use the form expected by ld, not + # the form expected by clang (i.e., -application_extension, not + # -fapplication-extension). + linkopts = kwargs.get("linkopts", []) + linkopts += ["-application_extension"] + kwargs["linkopts"] = linkopts + + # The macro below can't see the default argument in the attribute, so we have + # to duplicate it here. + kwargs["_product_type"] = product_support.WATCHKIT2_EXTENSION_PRODUCT_TYPE + + bundling_args = binary_support.create_binary_if_necessary( + name, str(apple_common.platform_type.watchos), **kwargs) + + _watchos_extension( + name = name, + **bundling_args + ) diff --git a/examples/ios/HelloWorld/BUILD b/examples/ios/HelloWorld/BUILD new file mode 100644 index 0000000000..01b6acad5d --- /dev/null +++ b/examples/ios/HelloWorld/BUILD @@ -0,0 +1,21 @@ +load("//apple:ios.bzl", "ios_application") + +objc_library( + name = "Sources", + srcs = [ + "Sources/AppDelegate.h", + "Sources/AppDelegate.m", + "Sources/main.m", + ], + resources = [ + "Resources/Main.storyboard", + ], +) + +ios_application( + name = "HelloWorld", + bundle_id = "com.example.hello-world", + families = ["iphone", "ipad"], + infoplists = [":Info.plist"], + deps = [":Sources"], +) diff --git a/examples/ios/HelloWorld/Info.plist b/examples/ios/HelloWorld/Info.plist new file mode 100644 index 0000000000..1af92630b0 --- /dev/null +++ b/examples/ios/HelloWorld/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/ios/HelloWorld/Resources/Main.storyboard b/examples/ios/HelloWorld/Resources/Main.storyboard new file mode 100644 index 0000000000..1bf5befe82 --- /dev/null +++ b/examples/ios/HelloWorld/Resources/Main.storyboard @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/ios/HelloWorld/Sources/AppDelegate.h b/examples/ios/HelloWorld/Sources/AppDelegate.h new file mode 100644 index 0000000000..25c2174902 --- /dev/null +++ b/examples/ios/HelloWorld/Sources/AppDelegate.h @@ -0,0 +1,21 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/examples/ios/HelloWorld/Sources/AppDelegate.m b/examples/ios/HelloWorld/Sources/AppDelegate.m new file mode 100644 index 0000000000..f20ba8a644 --- /dev/null +++ b/examples/ios/HelloWorld/Sources/AppDelegate.m @@ -0,0 +1,24 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "AppDelegate.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + return YES; +} + +@end diff --git a/examples/ios/HelloWorld/Sources/main.m b/examples/ios/HelloWorld/Sources/main.m new file mode 100644 index 0000000000..f1b040fa2f --- /dev/null +++ b/examples/ios/HelloWorld/Sources/main.m @@ -0,0 +1,23 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, + NSStringFromClass([AppDelegate class])); + } +} diff --git a/test/BUILD b/test/BUILD new file mode 100644 index 0000000000..7948f10e7b --- /dev/null +++ b/test/BUILD @@ -0,0 +1,67 @@ +load( + "//test:configurations.bzl", + "IOS_CONFIGURATIONS", + "TVOS_CONFIGURATIONS", + "WATCHOS_CONFIGURATIONS", +) +load("//test:test_rules.bzl", "apple_shell_test") + +config_setting( + name = "darwin", + values = {"host_cpu": "darwin_x86_64"}, +) + +apple_shell_test( + name = "ios_application_test", + size = "medium", + src = "ios_application_test.sh", + configurations = IOS_CONFIGURATIONS, + data = [ + "//test/testdata/binaries:empty_dylib", + "//test/testdata/binaries:empty_staticlib", + ], +) + +apple_shell_test( + name = "ios_application_resources_test", + size = "medium", + src = "ios_application_resources_test.sh", + configurations = IOS_CONFIGURATIONS, + data = [ + "//test/testdata/resources:resource_data_deps_ios", + "//test/testdata/resources:resource_data_deps_platform_independent", + ], +) + +apple_shell_test( + name = "ios_extension_test", + size = "medium", + src = "ios_extension_test.sh", + configurations = IOS_CONFIGURATIONS, + data = [ + "//test/testdata/binaries:empty_dylib", + "//test/testdata/binaries:empty_staticlib", + "//test/testdata/resources:resource_data_deps_ios", + ], +) + +apple_shell_test( + name = "tvos_application_test", + size = "medium", + src = "tvos_application_test.sh", + configurations = TVOS_CONFIGURATIONS, +) + +apple_shell_test( + name = "tvos_extension_test", + size = "medium", + src = "tvos_extension_test.sh", + configurations = TVOS_CONFIGURATIONS, +) + +apple_shell_test( + name = "watchos_application_test", + size = "medium", + src = "watchos_application_test.sh", + configurations = WATCHOS_CONFIGURATIONS, +) diff --git a/test/apple_shell_testrunner.sh b/test/apple_shell_testrunner.sh new file mode 100755 index 0000000000..4191b97196 --- /dev/null +++ b/test/apple_shell_testrunner.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test runner that sets up the environment for Apple shell integration tests. +# +# Use the `apple_shell_test` rule in //test:test_rules.bzl to spawn this. +# +# Usage: +# apple_shell_testrunner.sh +# +# test_script: The name of the test script to execute inside the test +# directory. + +test_script="$1"; shift + +function print_message_and_exit() { + echo "$1" >&2; exit 1; +} + +CURRENT_SCRIPT="${BASH_SOURCE[0]}" +# Go to the directory where the script is running +cd "$(dirname ${CURRENT_SCRIPT})" \ + || print_message_and_exit "Unable to access "$(dirname ${CURRENT_SCRIPT})"" + +DIR=$(pwd) +# Load the unit test framework +source "$DIR/unittest.bash" || print_message_and_exit "unittest.bash not found!" + +# Load the test environment +function create_new_workspace() { + new_workspace_dir="${1:-$(mktemp -d ${TEST_TMPDIR}/workspace.XXXXXXXX)}" + rm -fr "${new_workspace_dir}" + mkdir -p "${new_workspace_dir}" + cd "${new_workspace_dir}" + + touch WORKSPACE + cat > WORKSPACE < "$TEST_log" + rm -fr "${WORKSPACE_DIR}" + create_new_workspace "${WORKSPACE_DIR}" + [ "${new_workspace_dir}" = "${WORKSPACE_DIR}" ] || \ + { echo "Failed to create workspace" >&2; exit 1; } + export BAZEL_INSTALL_BASE=$(bazel info install_base) + export BAZEL_GENFILES=$(bazel info bazel-genfiles "${EXTRA_BUILD_OPTIONS[@]}") + export BAZEL_BIN=$(bazel info bazel-bin "${EXTRA_BUILD_OPTIONS[@]}") +} + +# Any remaining arguments are passed to every `bazel build` invocation in the +# subsequent tests (see `do_build` in apple_shell_testutils.sh). +export EXTRA_BUILD_OPTIONS=( "$@" ); shift $# +echo "Applying extra options to each build: ${EXTRA_BUILD_OPTIONS[*]}" > "$TEST_log" + +setup_clean_workspace + +source "$(rlocation build_bazel_rules_apple/test/apple_shell_testutils.sh)" +source "$(rlocation build_bazel_rules_apple/test/${test_script})" diff --git a/test/apple_shell_testutils.sh b/test/apple_shell_testutils.sh new file mode 100755 index 0000000000..46b4e05bf0 --- /dev/null +++ b/test/apple_shell_testutils.sh @@ -0,0 +1,495 @@ +#!/bin/bash + +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Common utilities that are useful across a variety of Apple shell integration +# tests. + + +# Usage: assert_exists +# +# Asserts that the file at the given path exists. +function assert_exists() { + path="$1" + [ -f "$path" ] && return 0 + + fail "Expected file '$path' to exist, but it did not" + return 1 +} + + +# Usage: assert_zip_contains +# +# Asserts that the file or directory at path `path_in_archive` exists in the +# zip `archive`. +function assert_zip_contains() { + archive="$1" + path="$2" + + # Build the regex we use to match by escaping a couple characters that might + # appear in filenames, then surround it by ^ and $. + path_regex="$(echo "$path" | sed -e 's/\([.+]\)/\\\1/g' -e 's/^.*$/^&$/g')" + + zip_contents=$(zipinfo -1 "$archive") + echo "$zip_contents" | grep "$path_regex" > /dev/null \ + || fail "Archive $archive did not contain ${path};" \ + "contents were: $zip_contents" +} + + +# Usage: assert_zip_not_contains +# +# Asserts that the file or directory at path `path_in_archive` does not exist +# in the zip `archive`. +function assert_zip_not_contains() { + archive="$1" + path="$2" + + # Build the regex we use to match by escaping a couple characters that might + # appear in filenames, then surround it by ^ and $. + path_regex="$(echo "$path" | sed -e 's/\([.+]\)/\\\1/g' -e 's/^.*$/^&$/g')" + + zip_contents=$(zipinfo -1 "$archive") + echo "$zip_contents" | grep "$path_regex" > /dev/null \ + && fail "Archive $archive contained $path, but it should not;" \ + "contents were: $zip_contents" \ + || true +} + + +# Usage: build_path +# +# Returns the relative path to the BUILD file for the given target by stripping +# the leading slashes, then removing the target name portion (":foo") and +# appending "BUILD". +function build_path() { + target_label="$1" + no_slashes="${target_label#//}" + echo "${no_slashes%%:*}/BUILD" +} + + +# Usage: create_dump_plist +# +# Concatenates a target named "dump_plist" to the BUILD file that owns +# `ipa_label`, which dumps keys and values from a plist into text files +# that can be checked using assertions. +# +# `ipa_label` should be the absolute label of an IPA file generated by a +# bundling rule in the test client (for example, "//app:app.ipa"). +# +# `plist_path` is the archive-root-relative path to the plist file to be +# dumped (for example, "Payload/app.app/Info.plist"). +# +# The remaining arguments are keypaths in PlistBuddy notation representing +# the values to dump. Each value will be dumped to a file in the package's +# genfiles directory with the same name as the keypath, with colons in the +# keypath replaced by dots. +# +# Example: +# dump_plist //app:app.ipa Payload/app.app/Info.plist \ +# CFBundleIdentifier CFBundleSupportedPlatforms:0 +# do_build ios 9.0 //app:dump_plist +# assert_equals "my.bundle.id" \ +# "$(cat "test-genfiles/app/CFBundleIdentifier")" +function create_dump_plist() { + ipa_label="$1"; shift + plist_path="$1"; shift + + build_path="$(build_path "$ipa_label")" + + # There is no convenient way to get an arbitrary value out of a Plist file on + # Linux, so we create an action we run on a Mac to dump out the values we + # care about to files in a genrule, and then assert on the contents of those + # files. + cat >> "${build_path}" <> "${build_path}" + done + + cat >> "${build_path}" < \$(@D)/${filename} && \" +" \ + >> "${build_path}" + done + + cat >> "${build_path}" < +# +# Concatenates a target named "dump_codesign" to the BUILD file that owns +# `ipa_label`, which dumps the results of executing `codesign` with the given +# arguments on a path inside the archive. The results are sent to a file named +# `codesign_output` in the genfiles directory for the target's package. +# +# `ipa_label` should be the absolute label of an IPA file generated by a +# bundling rule in the test client (for example, "//app:app.ipa"). +# +# `archive_path` should be the path within the archive on which codesign should +# be executed (for example, "Payload/app.app"). +# +# `codesign_args` is a list of arguments that should be passed to the codesign +# invocation. They are inserted before the archive path. +function create_dump_codesign() { + ipa_label="$1"; shift + archive_path="$1"; shift + + build_path="$(build_path "$ipa_label")" + + cat >> "${build_path}" < \$@ && " + + "rm -rf \$\${temp}", + tags = ["requires-darwin"], +) +EOF +} + + +# Usage: create_dump_codesign_count +# +# Concatenates a target named "dump_codesign_count" to the BUILD file that owns +# `ipa_label`, which executes `codesign` on the files at the given archive +# paths and counts the number of unique certificates among them. The results +# are sent to a file named `codesign_count_output` in the genfiles directory +# for the target's package. +# +# This target works by passing all the files to a single invocation of +# `codesign` and dumping their requirements, removing the bundle identifier +# from each (since they will differ). This leaves only the static portions and +# the certificate information on each line, which can then be sorted and +# uniq'd, then finally the line count of the output is taken. +# +# This target can be used to verify that multiple files in an archive (such as +# the main executable and frameworks/dylibs) all have the same signature, by +# asserting that the output of this target is "1". +# +# `ipa_label` should be the absolute label of an IPA file generated by a +# bundling rule in the test client (for example, "//app:app.ipa"). +# +# `archive_paths` should be a list of paths within the archive on which +# codesign should be executed (for example, "Payload/app.app/app"). +function create_dump_codesign_count() { + ipa_label="$1"; shift + archive_paths=("$@") + + build_path="$(build_path "$ipa_label")" + + cat >> "${build_path}" </dev/null | " + + "sed -e 's/identifier \"[^\"]*\" //' | sort | uniq | " + + "wc -l | tr -d ' ' > \$@ && " + + "rm -rf \$\${temp}", + tags = ["requires-darwin"], +) +EOF +} + +# Usage: current_archs +# +# Prints the architectures for the given platform that were specified in the +# configuration used to run the current test. For multiple architectures, the +# values will be printed on separate lines; the output here is typically meant +# to be captured into an array. +function current_archs() { + platform="$1" + if [[ "$platform" == ios ]]; then + # Fudge the ios platform name to match the expected command line option. + platform=ios_multi + fi + + for option in "${EXTRA_BUILD_OPTIONS[@]}"; do + case "$option" in + --"${platform}"_cpus=*) + value="$(echo "$option" | cut -d= -f2)" + echo "$value" | tr "," "\n" + return + ;; + esac + done +} + + +# Usage: do_build +# +# Helper function to invoke `bazel build` that applies --verbose_failures and +# log redirection for the test harness, along with any extra arguments that +# were passed in via the `apple_shell_test`'s `configurations` attribute. +# The first two arguments are the platform and minimum SDK version needed; +# the remaining arguments are passed directly to bazel. +# +# Test builds use "test-" as the output directory symlink prefix, so tests +# should expect to find their outputs in "test-bin" and "test-genfiles". +# +# Example: +# do_build ios 9.0 --ios_minimum_os=8.0 //foo:bar +function do_build() { + platform="$1"; shift + min_sdk_version="$1"; shift + + declare -a bazel_options=("--symlink_prefix=test-" "--verbose_failures") + + declare -a sdk_options=( \ + $(require_at_least_sdk "$platform" "$min_sdk_version") ) + if [ -n "${sdk_options[*]}" ]; then + bazel_options+=("${sdk_options[@]}") + else + fail "Could not find an Xcode that supports ${platform} version >= ${min_sdk_version}" + fi + + if is_device_build "$platform"; then + bazel_options+=("--ios_signing_cert_name=-") + fi + + bazel_options+=( \ + "${EXTRA_BUILD_OPTIONS[@]}" \ + --define=bazel_rules_apple.mock_provisioning=true \ + "$@" \ + ) + + echo "Executing: bazel build ${bazel_options[*]}" > "$TEST_log" + bazel build "${bazel_options[@]}" > "$TEST_log" 2>&1 +} + + +# Usage: is_ad_hoc_signed_build +# +# Returns a success code if the --ios_signing_cert_name flag is set to "-"; +# otherwise, it returns a failure exit code. +function is_ad_hoc_signed_build() { + for option in "${EXTRA_BUILD_OPTIONS[@]}"; do + if [[ "$option" == "--ios_signing_cert_name=-" ]]; then + return 0 + fi + done + + return 1 +} + + +# Usage: is_bitcode_build +# +# Returns a success code if the --apple_bitcode flag is set to either +# "embedded" or "embedded_markers"; otherwise, it returns a failure exit code. +function is_bitcode_build() { + for option in "${EXTRA_BUILD_OPTIONS[@]}"; do + case "$option" in + --apple-bitcode=none) + return 1 + ;; + --apple_bitcode=*) + return 0 + ;; + esac + done + + return 1 +} + + +# Usage: is_device_build +# +# Returns a success exit code if the current architectures correspond to a +# device build, or a failure exit code if they correspond to a simulator build. +# Intended to let individual tests skip all or part of their logic when running +# under multiple configurations. +function is_device_build() { + platform="$1" + archs="$(current_archs "$platform")" + + # For simplicity, we just test the entire architecture list string and assume + # users aren't writing tests with multiple incompatible architectures. + [[ "$platform" == macos ]] || [[ "$archs" == arm* ]] +} + + +# Usage: require_at_least_sdk +# +# Searches all installed Xcodes for one that supports an SDK for `platform` +# (one of "ios", "watchos", or "tvos") whose version number is at least as high +# as `min_sdk_version` (a dotted version number, like "10.1"). If found, this +# echoes the command line options needed to build with that Xcode and SDK. +# Otherwise, nothing is echoed. +function require_at_least_sdk() { + platform="$1" + min_sdk_version="$2" + + found_xcode_version="" + found_sdk_version="" + + # Query for all of the Xcode installations on the host machine, then scan the + # results for one that supports the given SDK (or higher). We don't have to + # find *the* highest version; we just need the first one that will work. + query="labels(versions, @local_config_xcode//:host_xcodes)" + + bazel query "$query" --output build | \ + while read -r line; do + case "$line" in + default_${platform}_sdk_version*) + found_sdk_version=$(echo "$line" | cut -d\" -f2) + ;; + version*) + found_xcode_version=$(echo "$line" | cut -d\" -f2) + ;; + esac + + if [ -n "$found_xcode_version" -a -n "$found_sdk_version" ]; then + found_num="$(version_as_number "$found_sdk_version")" + min_num="$(version_as_number "$min_sdk_version")" + + if [ "$found_num" -ge "$min_num" ]; then + echo "--xcode_version=$found_xcode_version --${platform}_sdk_version=$found_sdk_version" + return + fi + + # Reset our variables so that we can find the next pair. + found_xcode_version="" + found_sdk_version="" + fi + done +} + + +# Usage: print_debug_entitlements +# +# Extracts and prints the debug entitlements from the appropriate Mach-O +# section of the given binary. +function print_debug_entitlements() { + binary="$1" + + # This monstrosity uses objdump to dump the hex content of the entitlements + # section, strips off the leading addresses (and ignores lines that don't + # look like hex), then runs it through `xxd` to turn the hex into ASCII. + # The results should be the entitlements plist text, which we can compare + # against. + xcrun llvm-objdump -macho -section=__TEXT,__entitlements "$binary" | \ + sed -e 's/^[0-9a-f][0-9a-f]*[[:space:]][[:space:]]*//' \ + -e 'tx' -e 'd' -e ':x' | xxd -r -p +} + + +# Usage unzip_single_file +# +# Extracts and prints the contents of the file located at `path_in_archive` +# within the given zip `archive`. +function unzip_single_file() { + archive="$1" + path="$2" + unzip -p "$archive" "$path" +} + + +# Usage: assert_binary_contains +# +# Asserts that the binary at `path_in_archive` within the zip `archive` +# contains the string `symbol_string` in its objc runtime. +function assert_binary_contains() { + archive="$1" + path="$2" + symbol_string="$3" + + mkdir -p tempdir + fat_path="tempdir/fat_binary" + thin_path="tempdir/thin_binary" + + unzip_single_file "$archive" "$path" > $fat_path + thin_arch="$(lipo -info "${fat_path}" | awk 'NF>1{print $NF}')" + lipo -thin "$thin_arch" "$fat_path" -output "$thin_path" + otool_contents=$(otool -o "$thin_path") + rm -rf tempdir + echo "$otool_contents" | grep "$symbol_string" >& /dev/null && return 0 + fail "Expected binary '$path' to contain '$symbol_string' but it did not" +} + + +# Usage: assert_binary_not_contains +# +# Asserts that the binary at `path_in_archive` within the zip `archive` +# does not contain the string `symbol_string` in its objc runtime. +function assert_binary_not_contains() { + archive="$1" + path="$2" + symbol_string="$3" + + mkdir -p tempdir + fat_path="tempdir/fat_binary" + thin_path="tempdir/thin_binary" + + unzip_single_file "$archive" "$path" > $fat_path + thin_arch="$(lipo -info "${fat_path}" | awk 'NF>1{print $NF}')" + lipo -thin "$thin_arch" "$fat_path" -output "$thin_path" + otool_contents=$(otool -o "$thin_path") + rm -rf tempdir + echo "$otool_contents" | grep "$symbol_string" >& /dev/null || return 0 + fail "Expected binary '$path' to not contain '$symbol_string' but it did" +} + + +# Usage: version_as_number +# +# Converts a dotted version number string (like "1.2.3") to a number and prints +# it. The exact form of the number is unimportant; it is meant to be compared +# against other dotted versions converted the same way. +# +# Note that version numbers that contain non-numeric characters, like +# "1.2.3beta4", are handled gracefully here; the "beta4" is ignored and the +# returned number is treated as if it were "1.2.3", which is deemed suitable +# for the use cases needed here. +# +# Example: +# if [ "$(version_as_number "$x")" -ge "$(version_as_number 1.2.3)" ]; then +# # Executes if $x is version 1.2.3 or greater +# fi +function version_as_number() { + echo "$@" | awk -F. '{ printf("%d%03d%03d\n", $1,$2,$3); }' +} diff --git a/test/configurations.bzl b/test/configurations.bzl new file mode 100644 index 0000000000..46d8b7501f --- /dev/null +++ b/test/configurations.bzl @@ -0,0 +1,58 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Configuration options for the Apple rule integration tests.""" + + +# Configuration options used with `apple_shell_test` to run tests for +# iOS simulator and device builds. +# +# TODO(b/35091927): Here and below, switch to Bitcode mode "embedded". +IOS_DEVICE_OPTIONS = ["--ios_multi_cpus=arm64,armv7", "-c opt"] + +IOS_CONFIGURATIONS = { + "simulator": ["--ios_multi_cpus=i386,x86_64"], + "device": IOS_DEVICE_OPTIONS, + "device_bitcode": IOS_DEVICE_OPTIONS + [ + "--apple_bitcode=embedded_markers", + ], +} + +# Configuration options used with `apple_shell_test` to run tests for +# tvOS simulator and device builds. +TVOS_DEVICE_OPTIONS = ["--tvos_cpus=arm64", "-c opt"] + +TVOS_CONFIGURATIONS = { + "simulator": ["--tvos_cpus=x86_64"], + "device": TVOS_DEVICE_OPTIONS, + "device_bitcode": TVOS_DEVICE_OPTIONS + [ + "--apple_bitcode=embedded_markers", + ], +} + +# Configuration options used with `apple_shell_test` to run tests for +# watchOS simulator and device builds. Since watchOS apps are always bundled +# with an iOS host app, we include that platform's configuration options as +# well. +WATCHOS_DEVICE_OPTIONS = [ + "--ios_multi_cpus=arm64,armv7", "--watchos_cpus=armv7k", "-c opt", +] + +WATCHOS_CONFIGURATIONS = { + "simulator": ["--ios_multi_cpus=i386,x86_64", "--watchos_cpus=i386"], + "device": WATCHOS_DEVICE_OPTIONS, + "device_bitcode": WATCHOS_DEVICE_OPTIONS + [ + "--apple_bitcode=embedded_markers", + ], +} diff --git a/test/ios_application_resources_test.sh b/test/ios_application_resources_test.sh new file mode 100755 index 0000000000..01420203e6 --- /dev/null +++ b/test/ios_application_resources_test.sh @@ -0,0 +1,403 @@ +#!/bin/bash + +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eu + +# Integration tests for bundling simple iOS applications with resources. + +function set_up() { + rm -rf app + mkdir -p app +} + +# Creates common source, targets, and basic plist for iOS applications. +function create_common_files() { + cat > app/BUILD < app/main.m < app/Info.plist <> app/BUILD <> app/BUILD <> app/BUILD <> app/BUILD <> app/BUILD <> app/BUILD <> app/foo/Bar.bundle/baz.txt <> app/BUILD <> app/BUILD <> app/BUILD < app/BUILD < app/main.m < app/Info.plist <> app/BUILD <> app/BUILD <> app/BUILD <> app/BUILD < app/fmwk.framework/Info.plist < app/fmwk.framework/resource.txt < app/fmwk.framework/Headers/fmwk.h < app/fmwk.framework/Headers/module.modulemap <> app/BUILD < app/Another.plist <> app/BUILD < app/post_processor.sh < "\$WORKDIR/Payload/app.app/inserted_by_post_processor.txt" +EOF + chmod +x app/post_processor.sh + + do_build ios 9.0 //app:app || fail "Should build" + assert_equals "foo" "$(unzip_single_file "test-bin/app/app.ipa" \ + "Payload/app.app/inserted_by_post_processor.txt")" +} + +# Tests that linkopts get passed to the underlying apple_binary target. +function test_linkopts_passed_to_binary() { + # Bail out early if this is a Bitcode build; the -alias flag we use to test + # this isn't compatible with Bitcode. That's ok; as long as the test passes + # for non-Bitcode builds, we're good. + is_bitcode_build && return 0 + + create_common_files + + cat >> app/BUILD <> app/BUILD < app/entitlements.plist < + + + + test-an-entitlement + + + +EOF + + if is_device_build ios ; then + # For device builds, we verify that the entitlements are in the codesign + # output. + create_dump_codesign "//app:app.ipa" "Payload/app.app" -d --entitlements :- + do_build ios 9.0 //app:dump_codesign || fail "Should build" + + assert_contains "test-an-entitlement" \ + "test-genfiles/app/codesign_output" + else + # For simulator builds, the entitlements are added as a Mach-O section in + # the binary. + do_build ios 9.0 //app:app || fail "Should build" + + print_debug_entitlements "test-bin/app/app.apple_binary_lipobin" | \ + assert_contains "test-an-entitlement" - + fi +} + +# Tests that an iMessage application contains the appropriate stub executable +# and auto-injected plist keys. +function test_message_application() { + create_common_files + create_minimal_ios_application "apple_product_type.messages_application" + create_dump_plist "//app:app.ipa" "Payload/app.app/Info.plist" \ + LSApplicationLaunchProhibited + + do_build ios 10.0 --ios_minimum_os=10.0 //app:dump_plist || fail "Should build" + + # Ignore the following checks for simulator builds. + is_device_build ios || return 0 + + assert_zip_contains "test-bin/app/app.ipa" \ + "MessagesApplicationSupport/MessagesApplicationSupportStub" + assert_equals "true" "$(cat "test-genfiles/app/LSApplicationLaunchProhibited")" +} + +# Tests that applications can transitively depend on objc_bundle_library, and +# that the bundle library resources for the appropriate architecture are +# used in a multi-arch build. +function test_bundle_library_dependency() { + create_common_files + + cat >> app/BUILD < app/foo_sim.txt < app/foo_device.txt <> app/BUILD < app/BUILD < app/main.m < app/Info-App.plist < app/Info-Ext.plist <> app/BUILD <> app/BUILD <> app/BUILD < +# +# Creates minimal iOS application and extension targets that depends on an +# `objc_framework`. The `dynamic` argument should be `True` or `False` and will +# be used to populate the framework's `is_dynamic` attribute. +function create_minimal_ios_application_and_extension_with_objc_framework() { + readonly framework_type="$1" + + cat >> app/BUILD < app/fmwk.framework/Info.plist < app/fmwk.framework/resource.txt < app/fmwk.framework/Headers/fmwk.h < app/fmwk.framework/Headers/module.modulemap <> app/BUILD <> app/BUILD < app/BUILD < app/main.m < app/Info-App.plist < app/Info-Ext.plist < + + + + + AppIDName + Wildcard + ApplicationIdentifierPrefix + + FOOBARBAZ1 + + CreationDate + 2017-01-04T13:44:01Z + Platform + + iOS + + DeveloperCertificates + + NOT_A_VALID_CERTIFICATE + + Entitlements + + com.apple.developer.pass-type-identifiers + + FOOBARBAZ1.* + + keychain-access-groups + + FOOBARBAZ1.* + + get-task-allow + + application-identifier + FOOBARBAZ1.* + com.apple.developer.team-identifier + FOOBARBAZ1 + + ExpirationDate + 2117-01-04T13:44:01Z + Name + Integration Testing + ProvisionedDevices + + + TeamIdentifier + + FOOBARBAZ1 + + TeamName + Bazel Rules Apple + TimeToLive + 36500 + UUID + eaf3d44a-f20e-11e6-bc64-92361f002671 + Version + 1 + + diff --git a/test/testdata/resources/BUILD b/test/testdata/resources/BUILD new file mode 100644 index 0000000000..7922bd80f9 --- /dev/null +++ b/test/testdata/resources/BUILD @@ -0,0 +1,169 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "launch_screen_ios.storyboard", + "nonlocalized.strings", + "nonlocalized_resource.txt", + "storyboard_ios.storyboard", + "view_ios.xib", +]) + +# A convenience target that can be passed into the data attribute of an Apple +# shell test to make the iOS-compatible resources available to builds under +# test. +filegroup( + name = "resource_data_deps_ios", + srcs = [ + "BUILD", + "launch_screen_ios.storyboard", + "storyboard_ios.storyboard", + "view_ios.xib", + ":app_icons_ios", + ":assets_ios", + ":launch_images_ios", + ":localized_storyboards_ios", + ":localized_strings_ios", + ":localized_xibs_ios", + ":settings_bundle_ios_files", + ":sticker_pack_ios", + ":unversioned_datamodel", + ":versioned_datamodel", + ], +) + +filegroup( + name = "resource_data_deps_platform_independent", + srcs = [ + "BUILD", + "nonlocalized.strings", + "nonlocalized_resource.txt", + ":basic_bundle_files", + ":localized_generic_resources", + ":structured", + ], +) + +filegroup( + name = "texture_atlas_data_deps", + srcs = [ + "BUILD", + ":star_atlas_files", + ], +) + +filegroup( + name = "app_icons_ios", + srcs = glob(["app_icons_ios.xcassets/**"]), +) + +filegroup( + name = "assets_ios", + srcs = glob(["assets_ios.xcassets/**"]), +) + +filegroup( + name = "assets_tvos", + srcs = glob(["assets_tvos.xcassets/**"]), +) + +filegroup( + name = "assets_watchos", + srcs = glob(["assets_watchos.xcassets/**"]), +) + +objc_bundle( + name = "basic_bundle", + bundle_imports = [":basic_bundle_files"], +) + +filegroup( + name = "basic_bundle_files", + srcs = glob(["basic.bundle/**"]), +) + +objc_bundle_library( + name = "bundle_library", + asset_catalogs = [":assets_ios"], + bundles = [":basic_bundle"], + datamodels = [ + ":unversioned_datamodel", + ":versioned_datamodel", + ], + resources = [ + "nonlocalized_resource.txt", + ":localized_generic_resources", + ], + storyboards = [ + "storyboard_ios.storyboard", + ":localized_storyboards_ios", + ], + strings = [ + "nonlocalized.strings", + ":localized_strings_ios", + ], + structured_resources = [":structured"], + xibs = [ + "view_ios.xib", + ":localized_xibs_ios", + ], +) + +filegroup( + name = "launch_images_ios", + srcs = glob(["launch_images_ios.xcassets/**"]), +) + +filegroup( + name = "localized_generic_resources", + srcs = glob(["*.lproj/*.txt"]), +) + +filegroup( + name = "localized_storyboards_ios", + srcs = glob(["*.lproj/*.storyboard"]), +) + +filegroup( + name = "localized_strings_ios", + srcs = glob(["*.lproj/*.strings"]), +) + +filegroup( + name = "localized_xibs_ios", + srcs = glob(["*.lproj/*.xib"]), +) + +objc_bundle( + name = "settings_bundle_ios", + bundle_imports = [":settings_bundle_ios_files"], +) + +filegroup( + name = "settings_bundle_ios_files", + srcs = glob(["settings_ios.bundle/**"]), +) + +filegroup( + name = "star_atlas_files", + srcs = glob(["star.atlas/**"]), +) + +filegroup( + name = "sticker_pack_ios", + srcs = glob(["sticker_pack_ios.xcstickers/**"]), +) + +filegroup( + name = "structured", + srcs = glob(["structured/**"]), +) + +filegroup( + name = "unversioned_datamodel", + srcs = glob(["unversioned_datamodel.xcdatamodel/**"]), +) + +filegroup( + name = "versioned_datamodel", + srcs = glob(["versioned_datamodel.xcdatamodeld/**"]), +) diff --git a/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/Contents.json b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/Contents.json new file mode 100644 index 0000000000..5602180f30 --- /dev/null +++ b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/Contents.json @@ -0,0 +1,86 @@ +{ + "images": [ + { + "filename": "app_icon_29pt_2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "app_icon_29pt_3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "filename": "app_icon_40pt_2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "app_icon_40pt_3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "filename": "app_icon_40pt_3x.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "app_icon_60pt_3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "app_icon_29pt.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" + }, + { + "filename": "app_icon_29pt_2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "app_icon_40pt.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" + }, + { + "filename": "app_icon_40pt_2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "app_icon_76pt.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" + }, + { + "filename": "app_icon_76pt_2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" + }, + { + "filename": "app_icon_167pt.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_167pt.png b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_167pt.png new file mode 100644 index 0000000000000000000000000000000000000000..a9f9ff0d845237371015f9d40c7a7894e33a1a61 GIT binary patch literal 14792 zcmeI3L2TPp7{_17g$2rm!zNCaCnP{P&vxRpwwz2Q>1ZWVN*lBm2{G4xX{;u;v7IF? z7qp23aN~qvT(|%d;=qv|kdSubgoK0yCyq==oDtK?vz;bi(ztJ%c6^o-e|hi!ec$u% zeecB=f4I5v>SE!!0zznUeXX%YuU{pfC(qIE|KdMx)64l_Z8t*bsb`bV0{ZOMMTC}L za$7s`PV+Ux_IpLk@%Kn^*bAr{A@lMuuZIeY z9ff53XrpBx9oV`fUal3)p+N)mNNfqiUe}9^VO31y8q`jju_&aH_@FATBngF`=B7~h zLn3HJO|oTG5tj6#qG?M@>dS&ED`hMzSSd-0Zj{T0EDNJUtQDwbhEChqYFrtmqra+R zCyoOH@iDruUNBDNMI&~UA|6@+R*Ev7=j7O9pP(Oh)1f&wCSB4aUK~+BWv*Y)@nb*g z_&F)Z-MNYBoHU!`+;hj<>&;Ca#jDpTf)UcWo>A*CAb5*Jem}Iy>UDZ6m*(Al?6xP9 zGZiW6$sIcxx;bu1Pv(}ELN-Gty;q|i5-aw@mhX3KS(R;`AQbBLw5o&`nwIT)3D*}* zJnJ->@ku<=uwqi96Ld6PU#Z z?)f8Z$Cr)TJ-W>3$k znQUwZtp++WE1d%{X-&y8#9 zDBDak@Je>yY-8A_4+^AK(X^6g;@Rd|k4<+-x;qV*uHY!COQo_ja+>X#>$#ij33^7J z*<^t~^>i+?R{F?5pH}erabr3({cP92XN26mVRS2Gi!an24Z&-rhM=7 z`yYIJzRpH{SkrHRt^1;~&1xJGU?N-^e+05~Tyk&9(YPey&Fx{%`M5 dfXByApi1E*=b0NnQH}_$uWmHHd*#}V{{eS|GrRx* literal 0 HcmV?d00001 diff --git a/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_29pt.png b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_29pt.png new file mode 100644 index 0000000000000000000000000000000000000000..d3925e08ccb4b0320e7291c3be85c6bdbe084043 GIT binary patch literal 15317 zcmeI3c~BEs9>;sj+S*;F+&^}9S7$7%Qn99XrfNecfe54mQ%FSEAua=%U7T4*nxug) zLpqy;0j#Z>+1>Ks@<2sJxfSug5ycC^n?n#(K!PHm+yoMi1k(FDf$)f6&uq<~-KiwM zy!ZY6e(!zW@4fUtogBJ$&0@EY-4Fy>92_JK1K-Q(_pcX$PfWJY7kvFj6SPi;ARl~4 zzkh*b9a@SY-k%ck2z`VsM64j=+%Y8?g}W!lX@D9*B%dd0FvUh(&xyig2(>SFuiPx&^LsKeHT!qKsYP}Bl3Fh_F#FBba7fUXX(%HRWVz4GMnKSpi zZ``a6JN8tJ^AI(d=?N5gO(8Lw5~tG@$NSux-2^+fdS;>?Le_bfPeJds$y zjqY){*-OE(LJ}}naex-b^rTi!k}5w(m4(hAJkd#P`bpBkxC*u35p@)2?cV2NDRVw^5Gbe z(lrLyK}t-I{irCg7K)=(h@(ku9H#dp;;}6=xigTbk>zPfK-uMFf1^A2|8ah z=KF<>q33*tQcQg;@dw2^)9s*Ck zNMwg7lfmxqgqD(uczVA~hgSZ?(2)B(q5rBSz*&c>APqhGpPFKxFz=)aB6Ofalc354 z^=m)P!tsC3ZJp^*5%yC_gK2d*z2|(nGn>wNj5$v!+Q+_k#h8LV&-@hhmcW$~bWZE+ zu$-RLZp8n)fR6Z(`@%UAVsZ8RHk2cjUA1r!9T}}pz_hr345*18x7!_Y=Q_G3kf8KG zfQp#A6pq%pD>PHH@2;qXKG?*d8o$q+qN2p`k(Xmv}U%GuI9j)Mo0o<#g&YP9Fi|pJAKeI7Aqn|l2 z9F9!_(_pk&T#Sfd0gDT!!DzF%7!kn&78gu|(PnWmB7y}hE|>b}zi;EEv zEMRfLG#G6b7b7BAz~X{wFxo6GMntfH#Rbz~v{_t?h+qMW3#P$nv$z-$!2%W+OoP#8 zaWNu-1uQO@2BZC6TyC?E|KV!z%wGa{xbOT)bv$@5kfR6+lOf2aPY~pX|02i)1-`ox zB%Y5TJ&_0^-j5(l$cznl0uf{p5*i|3O;MD!uh-JmF=-qc92+qVb{U4cUJv&ez6v(T zgACzoCj0yPOasdcqm~t59~VS^l)s_F*xmK7_`ah2%m=6ZPbb(Y+gNQ?Tcq4JNniXU zu}kweEXn)!LUZMm#oGKgS!-CJAuQ0`P+$A1VR`=gG0Rxy$@X*Wlue;h!i=l&72XF z^5#@=aoy$L=YAt7QcX=w^%^HbQq3W0=Fs$>vC;9#Nz0#dzx^=xo8RSp-8Rq>oi+4D zs`+@u0M&c7@N7_Lzx6LErhel%H8nbMPin37|DrIw@lEsYlA+)fQ$pU5&9H^4kxo7I zuP^l{OTV{RZ6Vu7f|E^EO~ce!D^(LfRj(SYkWN|5_nw=Al1*QxjaV#`6lJs4%Bh-F zRJH$9U%5JaFmRjc;FW%==gd?i-fNr)+-mCT8>Otnwu$bp{)rczBP|^xeM93mt8r?y z-8TMq^XAQ4wrttDb?dfm+me%$Q&Lh=Q&ZE@($dq@w{PFRW5TOXx^=6(y!`g<+Z7cR zm6eru?%cV1_wK!W_wL`n|KP!chYue9c3goo!O+&$_VVS+SFc{Rx3|B3{ko&0 zqqDQKtE=nHn>TOYzU}Vr?&<02?d|RB>+A3D9~c-I92^`P8ZsJc5DZKRF|abgKkx=v7!f-I5)umt!NkB+u7A`yNt51J^u}u`@yWg4bB_Hv=iYen z{hjSsZdIPIAcSshZT5EQSx-MVAE)2HK6rJ7o}LUh4`PI#dOH2wKwo_MEJDo{x4)n4 zo3H71;8iUrI3(4H7g9As#@Zyb>=Tj*hvdlh+v493z7qx4X^U@brfh~?a_nxNMP%=6 zyKkSJ*eyp~>r{-1P6K!(v4n{?^kaR}7PGiIwbNlN3YjE1X^ZP=LSf(B5xPM{1g)w` zwyY{bqg7Qjt#2KV_CsUO;TEVy{^l$P#j{XLMmC906(Q$MjLk{{pBgIvVvk=TyhFmVH4NaI?E!6<2qVw$Kt ziu>}yGLat_>?lP%u|lj=WxUAAu}hzD6b-XWb8Jk8#3OzZQ$J;)Uw9lOL3|u6Nm))W zO-yTInq}^V*Xwx;Q^(21J&K?}y3jN3pN0hQk~kPeHrcpGZ{_x)yHDJ~v^X=7(wU{# zPA2XWw{#|V%UU6?kU{TN??%K*f~X$^!%p61I~NFrZa3>HVa2p;*H5{=XyAFLX~n1U zNY6@0hj!$uq$rZw>?^XaDY~Lba+CHY2$C!3Vwi#B4o*P~RZ^9{qUkhDU4=1#ay|xK zL5`JJ4;2M!K^(`{2SManNyqi9BZ5Q!$iUM05rB zSiViRdL24YbzMg%gH}^*I<~Z0Z#1R4eW*&7?6jm>O|3N>t!7JAtqh`RLibCddx1Sl z*Gm!Fxn^kKekt_pkxTbFYX~&y)n7Nof-sj-ZMre-&{NR4w12bRY>&J*J9;rObhBM4 zw4#`#YpyL`EV^46XAPuI?aWN4=0mlVtFl~;Di3kcfE=YrEb6iYB zP{46P8cdtxVj_Y9jtkOY+8h@X5fpG-kOtG{xR{8bfa8KRm^R17L<9vK7o@?oIW8t5 zDB!pt4W`X;F%dxl#|3FHZH|kH2nskZNP}r}Tuekzz;QtuOq=6kB7y>r3({cP92XN2 z6mVRS2Gi!an24Z&Fa;Qr|C1he{PpoW^u<8I-rO}2I(q@3_di7F z-*bBY9ifqo&>wf{ONXB$^i1&CyFc8a+PAiP>;1{kfBvN0KQHkuq8>zC_T*L1F6tt9bx-xppC-SgN#CyQ@dc9k<-PCk`+M_w ze=?VRytaDl^6cxg2%*c%OSN@+-HhKaJWoIG-2b&rFPHqK?GT|CUy0u{=!-93L#TMo zX>3JX^%dRnIyuw!nk3in_*9LMaii~>);@`ZCfRk|viQ%pKZt^3m&Hx3F4z4k*>jc- z1F~_r+OQ7yt&%O?sLUFDof>pVWD5OG+YR-8SxkI&8pq986cS0aUlteQg2GmPO{jVS z5wx5pS+c4Kg;Gw@v_e6BOHgGck7WfbbCOch^LbsCh3q9(W@%&uc1vHc-OS3-Z)I^W zihLd8Uayzy&E>pc7b~Sw3Ck*0Rf#I3@W74CzT}2iQKMopkq+(;80B zh*R7%b?l_?Oi7D-(zIkLq%&mDd)2D}F(WT%cwW1buClc=ghI8NtSaGJ-LxDx=K7|A z$CHLLKJ+6sGa?nbBIhMVkQ=7a3 zJ%DoDgYF>PjLau;fm+~WTYAe2I%ZUHI_56HzPoGSVIxou)|FoM+Fn41MJjU!p41Go zlC`EUyJ2Ly7Fn)UXhY6%Y`sMavPI^al2%lkQeLwy$+SxasYK@GVkvJEtvR1Stk+@s zIqRBdb>sb#S=&!(4bsn9zY{q0tTWp{6OaCBOH8miS8B-#=?XmnE0?a{aUTY}gI9%&s8Ymk|xIsbP8Q-6>? zy+&`3xEF1x)GAR;HwnE~)H4IJuuIp(lkN7@cQkRlfPv#*fVML_6>00}44tjn^D}D1 z2b)e;109)}%z|Rzb8W-QseUV-)pj@07gU+Puo8b}sjz!Ib^GE}*gc*)%zW2%qKcYT z8*2tx@#&i$3>)-8fmG%-ZB8@rc=LG1n$sukt(rr3a2T)4Vj;^J@0gglJuMM*WQpli zfuHnbCev2>z(Ai>@Ze!(bdm>K;TZ?RJ9@^((2f^@G?+2R#Y6-J92ca)j5#hQA}HXv zAPr{BaWN4=0mlVtFk_C3i3kcfE=YqJb6iYBP{46P8qApEVj_Y9jtkOY#vB(D5fpG- zkOnj6xR{8bfa8KRm@&u2L<9vK7o@?AIW8t5DB!pt4Q9-7F%dxl#|3FHV~&f72nskZ zNP`)3Tuekzz;Qtu%$Vb1B7y>r3({c592XN26mVRS1~cZkn24Z&V<@tRxT*jlcm342i{4ZoqlqMOt9T=n-iF znSODbHa0fSOfpH^q=|88I_ziHK#d@h14bRDJcg5;e7um*#BnXP zja&|)isQZ}lJn&{DPBaRlA@-&6A{FpLU#XS%oCNYXZ05wix9HY8eV-OqTxc<0e zVAIVgm*ba^$KtpN^njckc?L(S)#DsdxQM6Z3k96$*l>YJ6df&mi6i6-B2m5o6-4j^ zvEs-`F`v)bcyZ&yfF;qZ3dC8mJwzddHy+WKt8YjvI+x z+~}Two4*tS8IpjtilurSBei;kR$Ck&sIrVl2su)zzp6O<<(QJt&|H5ZK|`H3Grk!Q zmtiCx4=OT>ClK(2F$w`+EE0$XA|5{m)Fl)opbUv2*Q$tu<4_DCPbg3bL}Cyof)B@l zl%X-e4pLzx_LZW*S}2Z6DK60J)fgF1sIfvE)oBVP=w>6N4Azxcsx8*)!L-2gCFoAg zke9#K#HktsiD{I0sw^HfgcF2H91$rLDI;TryvP_KpBJgZB6u;P$S7VuUsNE767nMo zq5}}+a@c)aXqi@7LhqN2(5kN)8gkzjI$ckIvkohUH1zDh?hreK*)~)PVE`3+9I9MU zzy8xK8-IIi>!Xfh!hb61Fueh%_gozJ(WVO?W5H93_VMpsF{Y%?vv?)FC2*Al-O{=_ zt)S172x{W1?e;+2t&X7sNKpD8 zpqSX2ia_hu4EZ-csM+lqmCy&97*qq88RpM|K!R>-+Z;Kd&!MwgT;kV93;E!M3h3_* zSGYUWHTUtZaCfNdrsrV|LDKu&Khcn8$csLG0|!GkxKY64qeP+zkpvBG4)w?&jCgU5 zi~u{>K-Xm?SeM@?v|~rd+}#|Zjt$2^s-VAgeE=EMKfW-yVV6<6WjEGq~= zweAk{so#_~eDw6^c^CQ{eQuZONcuopvd3myTeoql^PfG3*_`&dbAKGoN;YLBFW3F| z^8Kd0=il0U{!OCxy&3aTSJo?o88TC5(s)501rNiLuFr$?(4wfqB*UuBV-~XIEM-9nS4bPn`L%@^?+2JyrF_C*5D#9p23MmgSXp z-LIQkc*@`*!d>_;lc zydF=~7X?2&cf{#-z5B0~SI^k%yRAM;XU)H5Pn~;1@t+g+b&vdvHT|^JzV4=cZfAR@ zvnc`lYlg+PRMWNk%4yr_i;KRQYN|u#ZB2Yolkn53+>hHY;D268J#8DFTBTeIRHw{$ zC(+aV^85cxd8t}CG4D*Ru)X!cvInec@EFx8^>s+SZ7RjejeSS>5-l+WerewFG-aCRh7UPsG-{opBE@+n*S7fZgW54{Tw zn$IlBCv82WtCYj+>C2}m-@MhCbkdexx#V_xJpD&~4)gS)YuIcXnYN9XLCV~|wF&R~ z^pz`DYHDh(UcFjdTU%FGS6^S>(9qD>*m&*QwWg+~>({T}xN+m=&6~~5&9`pdYH4Y? zefxH6YinCuTYG!^ojZ3rIyyQ#JG;8Ny1To3dU|?$d+*-8d+*-8zP`Tu_wV=j_YVvV z3=R$s4Gj$s508wDjE;_)Or{499*m8RjgOB{OiWBpPEJitna$?u>FJr7nTHP_&d$!x z&CSiv&o3-2EG{lCEiG9r7OT~|yu7@!va-6mYO~qwcKh1e+WPvs!{Go0?sB=@^g3y} zJ|8G3Dq`P^2ke`Dsj>uxaVY;wobv;|pz_7%4#)gMrx^eH^~~Sz`{lQRfTH#wXETwr V?|$cbHTa37CZ)+PCLaCm{{RiE=%oMv literal 0 HcmV?d00001 diff --git a/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_40pt_2x.png b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_40pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..32bd0d8a82af718990d319b062a7184396a9fc44 GIT binary patch literal 14640 zcmeI3F>K>h7{?z)hkAhJI8)$ttPgy zor~LvI|g96fe9goV`PAZk%5Vg;btZnK}aXDA&|q_2l=e36oTdGG&y-}CSL z-isH1*4TXKQtoOFA#`bDy|zWa8_9eA1^W5$@?YQ5uZ#ZrZivu}FDLKk(3fAmf>3eM zY3@Wj^>=m4>*h_{Ymt21^{E;m<9h6y)&Yrx7TI&$viRhOA4S2j%i=AqF4z4k*>~2D z0Wi)mb)+Q~2$g;Ww9l*QE~p|DeL2vsj2 zf|l1LOI8(OrIc4RZDmD$Q&454fMo?MOOjI33k6-4g~3a#5wjQqmcS3Q~msXFY>~D zZ$`@D^vuMxCiVI-_tf=vyHit#(b_$VV1RV0XV^US3Em>1*9$DNc8^}lwP{x$Iqgw# z#v&y%GuKXHXNFrclewj>kX6W_*Q!?oVn$xj^t?_b>$1iPLZMnsyGmHBo0j7yT;DM8 zq|>P4qj;odMx;VJa#>OoNi8-NS=SU@(ImM@`w|4nloK)Pp6#>`K@3$=m8PQUG|bX6 zi~*FBG3XAm&B#1g6sQGpY)fx@LD!5bPS@Nc*mw5~JQ@VbVPEN0uj2)DS){UL;Az9a zEA2IX!wn`tX{HT2BGbz3=P~*g?=w^=vilWfF@b})25gb=2WV6C!`&E2s)ScZ+e<-lMly7 zPbNA}dMf#55RzoimBo`yH$29Mr&QvT?p@uqlJl%$C0l~n1|Az7Eot7U^}RK9dtu9d~Crf{B4+ zRx0q9o>np&r8f-pUIhq4w+zQXw7+ujb4h+NbB#;Kv=D3)Mpn&6oG?+HW#Y6-J z92ca)v^g#&A}HXvAPuI?aWN4=0mlVtFl~;Di3kcfE=YrEb6iYBP{46P8cdtxVj_Y9 zjtkOY+8h@X5fpG-kOtG{xR{8bfa8KRm^R17L<9vK7o@?oIW8t5DB!pt4W`X;F%dxl z#|3FHZH|kH2nskZNP}r}Tuekzz;QtuOq=6kB7y>r3({cP92XN26mVRS2Gi!an24Z& zo#vA^i)4u6Uo{`ROLrKG>+OHsfFZe5_nXXdZq2W)@E=Rn#$^Bi literal 0 HcmV?d00001 diff --git a/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_40pt_3x.png b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_40pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..eb969912b7ef878b263c691842bb10699692f384 GIT binary patch literal 14722 zcmeI3O>E;t6vwBmSg~#QutF*oC*u_eR(1T5I3JdiRY|(Eh?J@gA{7Vf*pt?3VjJ7p zq!ou1d*aFo0e5a_58OD=3lb71_67&;yFx-jTvkFVV>=%&sngj-dz{EgUS|I9y_w&< znfT(*cedZTzWC}QLg@O|W^I>z52E)=SIK9|%0DDu*SyXB0HLLqqxS;(>YG)Binr`W zGi=u1(@eLUF)X)*GlQ;2)ClRf2cBUbak|J}3QbtzQLP1&M6iLpBk}S$uK`v>zoF+-!@WodaiKY8iTidPO8Kxt@%KT9n zdYUNq`~6Hmn{oX^Q7)BAqNIq5A`pcToH(H|5S-v<5@Zyoh6B^Lz0h_YE{bck++JAb z`6$u&HJn%18z*vtAv;1*92lM`XCyJr$uh@2UeE8uL$gc~cW@UwVL<%kR6p;?4c*|# zos)9hJvT8~lX`ueJ9WI>Zffcv+_*~+43VaK28|OBi@P{*d%lS`?vkn8Oq+dZw$qMOYu3FW{M(Fwt*X>l2D%%+$rfqle5m~08KRdXHAh3=0d7XJ0KN$0xFl(Z+x2V;ZLSp zM;#qIK9xMf4{)^S%KT{4jgPVMDHZv|dsj2e=sc^K(U!oLE>5*hhBfvay4nAC0h9Q^ zeSVJq5q2)yP)Vp*H9tt;w!^;Rlep84vk7!D`UmLP(@T-GPOs3YW-qR& z9vy5NsRlAKBVGkr7caC;GAH_Gw5pw6tS=}Md5LoLIdp~Hv#$G>yTa~S*NNu`jvZE% zVYZoO;1!*|$-%HkZWM4Ouc}#97iXJiJ$CE??lfyQ*}*|nm$E`87CX)Mq8Ps5ZkzMFa&57oQ4v7_!v$$jZH9}A2nrZ3NP}uKTvS9*z;HntRGZj3(}z43>Os< z6fj(n2GwS`sED9|;es@%Hp4|l1O*Hiq(QYAE-E4@V7MR+s?Bgw5kUdN1!+)ihKq^_ z3K%X(gK9HeR76m~a6uYWo8h7&f&zvM(xBRx;#!0{^zgjHxOFBFa7)V zAD{m7_ZfQh`1_CEKr06e$#r$@?1ki7Zr*!wbS*4BK*@C_MY(pCs?0BpoR*|f_?3fE uc#>q2VKq&;kT#ap2iJZnlj)qFUqRoZl^eI(@4iNUqOFbX+IMdseD)uFMcB~* literal 0 HcmV?d00001 diff --git a/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_60pt_3x.png b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_60pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..daa987c13814cfef015cbf0171cc1e94365d9d2b GIT binary patch literal 14877 zcmeI3&ub%P7{}jTp{&(j6x`B-46}$oa56udG|4c@Qj@L?Y-(vErl;*>=1ns+nHgs$ zo3y9J1z`^&cv{5E9>kk?5IhLFd(fL8A|AwlKw0p-r~1y1OrE4k-|gDt8%XlxeZSx5 zdFS)~$Xw>$_SU;giz|x=p{3epb%%U^8@{hBkk8UD3kvyq-rd~u5qja}@O=q={>3sv zg}1CmGicUts)p0c=%&-cnPJZ*YJ{}wLsvHralp0kfn}HYf4~2c=Pa|te~_z7b+>{$ z*5Vf`LS)dl=F%7ltcs)HRTRr^%i>`g3iQ`6~OxKlKak`F2hK0+k znmDT&xT3YD)@(n}Z3EY;WzvwbEK_acLa}9L3qnEH4I#UVO`(`=Wrej|zSzQsQfT2~ z1hHO+_EV{=j?oYIOOo0=XKG+SmHIu;B4?f61)6a5=PfZK&8bqGmQPmb5m>oo{YIzR zF8*+`b=1+dqEpG$Js*dAuEdWvU3!eAr&Q>P_O7ZM;dxdz!YzSKO`K>Q4=Z&hHtSg@ zFwPI`^J@$`*gk7R#i=6I{3O294hFi1*AK{=c)Hyl=bbc87tqA;AE0YZPDR{0IYXm0 zdwNE-@L*HPY9J%iqgjwO@l@Nma;$HLv)b)P`n)2Mmnesyi4}HFTldbk!tQD7*!jL~ z1!X0vHq{JV;prP647=n;0hiZuxz(H|PB%|Gwyh!VHmerd!G5?dTz9jTtT~A}C8Ps4>GuMFa&57oj3(}y*3>Os<6fj(n1~q25 zsED9|;es@%F~dbg1O*Hiq(O}tE-E4@V7MR+YJ4WH#qk4x*d|Z?4anntKfM0eSLD$^ z&e+_kBXs;4LU-;W^yGwm|BX;zLg=4;gw*>8U3EUY_0tW6F7;~F^~Uhe$G^!-2wi#j z&6E7ntA*O9E6qQCe|YuYod=J8-S0@*j~6O;qwvM0BwSueLOGj+2GK>~&yj_MpN!nE zT>d}y<&}$Z3zwf^>}l~7!g;Pdal7Aq51W-+BR!9_;=%R)Ck!(dm(X-Xrk=XrstQ{f4=)(RXn$>zFVm4^{`40y|t5w z?wo8koRcG`S0opVnO0V7WW2;wI)3_G1c{f&-RFWK()fJvl*{yFW)gYou zAy?2G-87V9DQ6T4#iDsbF?C}R>jpORno+VA7cE^^`X9A2$1FQ?53KFlt$sRoRaOs^ zB(yN@cDuQ5J{LqyY?MkRtee<0HKx$wV?QB1&5y5VLC)jUXzWB@n0SG&@VI0@=pK_Wlyvttx-kA&FB>3EWp>kNFtPSi@L<~o?RXq);;%>0are&Jz|1o2@o zCFNjvYGSq~_4*+9#QC<{6H~{@>RpDQk94AE+&B&?-llQTi5$9mm)*+sNq3)k2gAh~ ziNr^y&YkwWDQt5uCCNrI>m1g%O|Wn1S6m1;GuDrKQg9M9)mU$gPJ z({ROy@o0@Cw8AQKNiz)1Ty7Y;RWK~0py|u3EOX#Az-W%Zk$W;^u#(cbfc zmY1GNA&Fwj_gq;$-*kgxY;a0(pLFk9#Np>z#o=3mx;7r^9ZqZD*?05)?*eA=f&26v z-9zeMwxP06scL$VI5Mye-sn>wJby-~M`-~4vbljWf2nPC&*;0YO z^mHY&UiQGio>lPRVP$lY2V3DO8^b$#%7I}xnFP`h+7g!#5fn&VkcQBfxP*wHK;nWl zgto*bL<9vA7o;JyB`zT%D3G`y4WTV@2@yeo#06;xZHY^W2nr-FNJD5#TtY-pAaOw& zLR;bzB7y>m3(^qU5|Z@Ae0upRyMN6=!X`j?l>~ z2z~r1LXXZ^`v*cD9icz(AY?s2=;h%4dq3V{+7H)jD~;Z7fBj<2BQ*2Fw~vbRFZI?> zetqMsYk%KYHXi8Y;ft^GX66cLPG{4rw7L2aG%t|!Dr-Jt&TK4esyx)`EN?!#!4jXH aJ&T(6{_Xr8{Jg*}(faCU?VC66eDWV*Bh0w~ literal 0 HcmV?d00001 diff --git a/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_76pt_2x.png b/test/testdata/resources/app_icons_ios.xcassets/app_icon.appiconset/app_icon_76pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..06b4b995d5211a7deb848bfb2f5bbaa3be45816f GIT binary patch literal 14810 zcmeI3&u`;I6vwAjs!C~3d!X72Uayd#!tsyRO=CG(m8456ky6?q+5@7lJ!!2bwy~W} z+S5WMq`mG5t@sN#toDe61iP#_aYN$5i7P9l7010$#y{%3I7z3A_BfGJzrOjt_r{-j zGx5duwl+VQo4G!N5SrUquWgg-H_>-`iu|4bnY%$QuY2oz0YYzFi@p=+!MF1Wtt{A$ z-Eg=5p=P@6f?>G_xX^2RM2(PstLGW!F%G!{d}upm{`n6-@tkdy`A^llRQIa*$X-A7 z@y_XH!#q7UOBR2tGNbo2lAw)4gX^_hPN4P5e4JM!cH|a$E|!GHWqvg($nDm*xT@=8 zPA#Z{DJe2nEEQx`Ef$scI7O0|L`fFqML{lUOG}z0ap{Mz%n(cWt){kJyPcLJ|CRZp zF!VH0>~_0_?qb394@J3DDv6RJDvCf9LU7`QMo(~pn@N(3JT)AczU_s!>u^zC;6Ysin5A9}u zaRxF)BV*@|d-j;HXe0@XTOnB?o!qNd^|2AUe#3QJm88qIE)a6nYTQ-aJ9WdforvqZ zx|k2@uXsNnt{EY&kd9myWLZ#F8nUFRvL>s7v_kq4BuSLH9Cg>Sn3{KcU6 zVxVQmr;=y*0gm=unZMX{*)f)#Qc+C2cQwO|&a;XcZ3%4Y;()h5tt>JPv;Xe`Ciy}5 z_!`|K>|C~?l2oy3e3HOzhF!zQtB0f~uD08gyn})H0=gLe0<`SGrAWMkD->^h(zC-W zsz(Q#M!JE_%!pS(*2N)TzjC7AjaIeQiS;i4jf z0)`9HpxO)<6%iCLT#yFUX1J(`pn&0mG^jSiMMVS!3>Tz9wHYodA}C8Ps5ZkzMFa&57oQ4v7_!v$$jZH9}A2nrZ3NP}uKTvS9* zz;HntRGZj3(}z43>Os<6fj(n2Gzb4*G&HfK)+p>o}WGY@`*G*HF;)8vr|(~_vdG4zkc$VTyp+>>B@eR?&*F`f9*9= zV8j5aGvvQ_m&|Cy0GU9}pG}zVAH2a-e*IzOL855N2U-_M>;J|cwC#@c4> J;rshv{Rbdg{#XD2 literal 0 HcmV?d00001 diff --git a/test/testdata/resources/assets_ios.xcassets/Contents.json b/test/testdata/resources/assets_ios.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/test/testdata/resources/assets_ios.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/Contents.json b/test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/Contents.json new file mode 100644 index 0000000000..4969448dc5 --- /dev/null +++ b/test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "ipad", + "filename" : "star.png", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "filename" : "star_2x.png", + "scale" : "2x" + }, + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/star.png b/test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/star.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca1b8a9f680cf9bb08e9e1145bfc13ce5c898c8 GIT binary patch literal 802 zcmV+-1Ks?IP)2!D1_kGom>NLPU zY<0AYr0s(ODlOO_>ls9sR=AaAoR*h5fw98-*clw)mXYy)P2i99UIQC)c+`M28ArYp zSyJtQRCrHT9_wLOkO@=nV$THrXwMb0;RI%R1#lD?-+$lg$P9OHdY7bcE zG4=*0u`R5D(xL@s+6F{*PFqd^ho?$4fQJWLYz|;m9R42{S|D*bq_;WQXY9~Lg{R7n0)me3;Tc zJn)}$8TMh{x=zs8(M2;29u?J$hNg>LQRNs|^A04Sb;|`)R8vs2e#M}@dR$Q@Dq1tb z-6}4;ngrs&>n|%9;)t2GU74pbGiN(|v3G@AkKdL+0H`3()sPsg7l`JqM)O(clVZst ziuxhLZz8P;)@DwEB_`mPkjG5Izp|u&xYZjr6Oc){1Fj^80%(^P9UCpP@QcQ>nQr(> z)Akn`i*M6h#u{uB?xt^Jum$zkQw@SXXoQMfKmT7qZ=5o{YgmL;byE+0?9@Y_x@0WA zD8?8mc5N;g&&1)1^@s607*qoM6N<$g05X*J^%m! literal 0 HcmV?d00001 diff --git a/test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/star_2x.png b/test/testdata/resources/assets_ios.xcassets/star_ipad.imageset/star_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..78afe5cc223fbd7b7cd817f2bbbdb09b060a0daf GIT binary patch literal 1882 zcmV-g2c`IlP)hVhaZjft9I)F5En-J%i+ax1s8yIpqX`2A;gw!7Qe znb~$$Jk9K!%YXj=`~LHv%bb#EQOc*|&zMra!|FAnXQQd?yR9&#wL+Ir{CKK zxT}=+7lQyl7zM7g+L%soDNq84bz)B|c(;`>1jzBjc_eETB-aF%0ws{&pL`t1)=&g1 z&tkIzlJi{0ZDpBC+e|Zo5m4TjvR?sO29H;3<~-LSL9U-=RUv?%YBMuZm)oD%0(5s$ zSPE^4F+~^GaU07h*(nm!s|bt$0=(>ffOpcWMxjj1mYp8vnqE$(Y6Scg3ImDF@a*02 zu+(mzjQ#q)+{Q92tNPRnOrsnK0aKdF`*f>8LgOW~K|-67L9^Ybfu!&1851b%>)Z&R z-ou_H7w_AMScYY}5BIg}r~7VtHIRd==jV-Lf}~6!SP~>;3O-M0oh3obrcPcbVDf}6 z%+U~U5gCk08eYsuqD)k8&rr|NF{eI_>KqBk&*F1(<1#^}u7DzF6Ht(3pori3^~$JS zNNwE&3BA6hK^e@=A219>wawKE5sfi1RF04!MNDc&CN_&Q^l-qFn|!0b0LXRZh=dUmlKPO@i&+(- zxd6Kg;~H+(YJl=^5V4cyTF0nr&2j?!;4#GOMFba736b)&7VFCnN=-hAiXu126QSxp z|EoM>D}ygGX2y=OvQiIAcckG9l6H&t1MO*p*AYl%{^QRcR$m9F^u}E+BOUBEoh5%RO_jNreh4C?# z&JMycSX+6`L5d~+gM_JI4z&sS4}2~N>z2MH+Bf`lwdE-UxG|qj-Y=zj5F}>PO(3!A zI*?%S#{m9}0)K@_+Z0WWd{+&g^Ns|#AvX|Thxzb1X8H}Z5aF~M;nYx?djyym{85C> zS*+(IE+OtW#(J|qI$3aqbT!?zR>%^^+dOfjDfDiP#Eef*PB5zKuuB_9+Jrc-3|$Zu z+R%|2{;5i$ChJOoC5wZJyRMbwS6G1;(0qi`8jOV7iL*w1lLeE6`5Qbx#^C8(TZkTN z@5%h?rp42rtRSSdt%s5`tk^t^eWGqhbTi<)4LVu31zwzCn_l2qw3itkgB-E}y zfb1!G0VMtiezRkpteb7HEw;%8ujGpWD+zBK8NxdJKId`z3_RI7*#_J4Yrr>wvclE; zrVh-sfK!!DWy7!frU+nl>t@BW*`u=$G`3FLhL0{v#5V!?NpgYuPT-|kT^0H@;ZIKK zRlcmG`Ly-nX&Zx@}u_z1!RW2#{0+L<{1-0{SeN<-s} zZvyy~vnKE?M5*7zqD_cXS+{M=j~U+!I*!=o5%7nI4`KFxq1}!f3mghPQ#m{CXEjN?!{K6y$pkK%0e7e2gK;2454Rik92I^2P@yq7fUbVhW73uD%W+d;Ph_^~BTXGchfcWsI)s&B zEzLs+UB2!D1_kGom>NLPU zY<0AYr0s(ODlOO_>ls9sR=AaAoR*h5fw98-*clw)mXYy)P2i99UIQC)c+`M28ArYp zSyJtQRCrHT9_wLOkO@=nV$THrXwMb0;RI%R1#lD?-+$lg$P9OHdY7bcE zG4=*0u`R5D(xL@s+6F{*PFqd^ho?$4fQJWLYz|;m9R42{S|D*bq_;WQXY9~Lg{R7n0)me3;Tc zJn)}$8TMh{x=zs8(M2;29u?J$hNg>LQRNs|^A04Sb;|`)R8vs2e#M}@dR$Q@Dq1tb z-6}4;ngrs&>n|%9;)t2GU74pbGiN(|v3G@AkKdL+0H`3()sPsg7l`JqM)O(clVZst ziuxhLZz8P;)@DwEB_`mPkjG5Izp|u&xYZjr6Oc){1Fj^80%(^P9UCpP@QcQ>nQr(> z)Akn`i*M6h#u{uB?xt^Jum$zkQw@SXXoQMfKmT7qZ=5o{YgmL;byE+0?9@Y_x@0WA zD8?8mc5N;g&&1)1^@s607*qoM6N<$g05X*J^%m! literal 0 HcmV?d00001 diff --git a/test/testdata/resources/assets_ios.xcassets/star_iphone.imageset/star_2x.png b/test/testdata/resources/assets_ios.xcassets/star_iphone.imageset/star_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..78afe5cc223fbd7b7cd817f2bbbdb09b060a0daf GIT binary patch literal 1882 zcmV-g2c`IlP)hVhaZjft9I)F5En-J%i+ax1s8yIpqX`2A;gw!7Qe znb~$$Jk9K!%YXj=`~LHv%bb#EQOc*|&zMra!|FAnXQQd?yR9&#wL+Ir{CKK zxT}=+7lQyl7zM7g+L%soDNq84bz)B|c(;`>1jzBjc_eETB-aF%0ws{&pL`t1)=&g1 z&tkIzlJi{0ZDpBC+e|Zo5m4TjvR?sO29H;3<~-LSL9U-=RUv?%YBMuZm)oD%0(5s$ zSPE^4F+~^GaU07h*(nm!s|bt$0=(>ffOpcWMxjj1mYp8vnqE$(Y6Scg3ImDF@a*02 zu+(mzjQ#q)+{Q92tNPRnOrsnK0aKdF`*f>8LgOW~K|-67L9^Ybfu!&1851b%>)Z&R z-ou_H7w_AMScYY}5BIg}r~7VtHIRd==jV-Lf}~6!SP~>;3O-M0oh3obrcPcbVDf}6 z%+U~U5gCk08eYsuqD)k8&rr|NF{eI_>KqBk&*F1(<1#^}u7DzF6Ht(3pori3^~$JS zNNwE&3BA6hK^e@=A219>wawKE5sfi1RF04!MNDc&CN_&Q^l-qFn|!0b0LXRZh=dUmlKPO@i&+(- zxd6Kg;~H+(YJl=^5V4cyTF0nr&2j?!;4#GOMFba736b)&7VFCnN=-hAiXu126QSxp z|EoM>D}ygGX2y=OvQiIAcckG9l6H&t1MO*p*AYl%{^QRcR$m9F^u}E+BOUBEoh5%RO_jNreh4C?# z&JMycSX+6`L5d~+gM_JI4z&sS4}2~N>z2MH+Bf`lwdE-UxG|qj-Y=zj5F}>PO(3!A zI*?%S#{m9}0)K@_+Z0WWd{+&g^Ns|#AvX|Thxzb1X8H}Z5aF~M;nYx?djyym{85C> zS*+(IE+OtW#(J|qI$3aqbT!?zR>%^^+dOfjDfDiP#Eef*PB5zKuuB_9+Jrc-3|$Zu z+R%|2{;5i$ChJOoC5wZJyRMbwS6G1;(0qi`8jOV7iL*w1lLeE6`5Qbx#^C8(TZkTN z@5%h?rp42rtRSSdt%s5`tk^t^eWGqhbTi<)4LVu31zwzCn_l2qw3itkgB-E}y zfb1!G0VMtiezRkpteb7HEw;%8ujGpWD+zBK8NxdJKId`z3_RI7*#_J4Yrr>wvclE; zrVh-sfK!!DWy7!frU+nl>t@BW*`u=$G`3FLhL0{v#5V!?NpgYuPT-|kT^0H@;ZIKK zRlcmG`Ly-nX&Zx@}u_z1!RW2#{0+L<{1-0{SeN<-s} zZvyy~vnKE?M5*7zqD_cXS+{M=j~U+!I*!=o5%7nI4`KFxq1}!f3mghPQ#m{CXEjN?!{K6y$pkK%0e7e2gK;2454Rik92I^2P@yq7fUbVhW73uD%W+d;Ph_^~BTXGchfcWsI)s&B zEzLs+UB2si&00004XF*Lt006O% z3;baP000YENkl8K2qp>&_uDsY2BzDQ)btN#f8H^AIY30OB9skI=@p zOrdI}ic+9RsDY>v1)@r+kO;3LP^3yx@gpdz015sFeiq7H;NZD>;ffv1yu{-6iAlJ=)5H3#0iq}5ev=cVQr+fT~vFI z&QM;2yh@-lR8~JH6(?R{apEWY;SmGMz&hTejSqOy0F~8wNyUj06!acWbDl~ws%bD0 zXv~Y!@S;QsDo#B4N%SbY6dBZnw)st{CP8?qWW4TK?s9e1lV1Jzi4laQ1183P-@~_K zlxpNX+SWibbiGI>wuVfMpyE{j$6@mxWp_yL(Hwb`NcY8_iY}eKrg0(!Den>TkhX-D z6s+AH!u7j`F=SPlijagZ(%G=gDrF`@P+=ra6t(*Lw3ZjTJL(Oupc< z1l$4=yBvs~14O9WwaePdx>=D4sG5g2l5J(U9y{n)(O z+z6$dlOBdbAFkKgQ{uHxFSwkPCmPV=W!TtrBM}T-ilh} zFNU(^tMh#hclt4L(cX~5n32P(@p`jx=yYW+W8JcHQ5sr@At?T7Gx;LCXn6&EL#jcN zvsYmod0pQ1I+nMkJ>BuT4Kw`1rwe9DUYO|pG6efkmGTCRHaEg;Td$dVvn&eY!!m)m zl_A|k*DSq+hFk#DVj!k>5xP1wS%Bv+qgs&1@_3x)3*)^=hYV;YECUrFfMtYBh9U&- zI>W08Sz9bIxk$u81;}zqw3q~u>Rq$uY1G^XU-=byd%y8cdBO@WAg}0JUnEpv4m_wY ztP?MLbaU{Vw=C80IwFre&kW$EEo*mu4Chim#mp3wIX76F;T^enqm9SHj!;)qaXNj;()l&?*hFNxWK+|Ve-izBXN%>^ajDK zT_ALYw3fKWVnt3nlNUYzfC2nic4X{6&L63oAk;LOwi2+0`hk%0v)XjN1Nha}L<~5K2InFa! zro84G00O>~9X}S^4^!;2ykeENJ(Kl_mE`bh*roqN@m>(lER!$ z=t4=xSnZ3yf}AV9nVeY%vZHyw+h~HvFWE#;XBWo%9>k3M+xnSrgeTpK(QUnvq{u8TN>rGZ;h`Tv#wr zaEsrU8J;;7R)r}SnIKY9ob0_z3igCY6847d+wk>OE@GM$C6cD?*fVhB%C?0|4!b)u zeExXU`uzqPL4#YdHHH}|Tn7Qb#HCm&BcSk$sJb;d3X?S!haA!z+Zv1r6X=Vc(J*=U zirKo(TJzYfn=lc!xRu5xh!(e4!npT+fQF;PPAiyo6Qc=SViL9!gZ%BU+gkwgp z115^OxIc!8MNs*|!kgg6Y6Ygwnqq0wX2pzwK_74uw(?BDn}S5q9Lpp}h}~YLhHHE8 z(n=Uu9A=EN5%9z!2#Zy}jmMoJqGiyH4Rve7ZzT~-LJ zg{`;%2!TrjM3kgc-GAB4+Of>|meyPf`6r}^O@bh{xOeYia&mD~80F#=Y z*%=3Irx~@nIE4(tis)2OCd_kWg7C|WLERIoYj-tjqZ?hS^F=3Y25g=$ft+Jq9qr%b z`_8>$=IL~Wumkes&ZDLB5^mPBd-a@)h<08BQRu`~XI}9ZU6x|P)Pr}p*~idX-wa=?Xsds$8J#Pj&Z{HuT}8%^mq_`(lnNAiDMsVVxK!bI*J zpyp9Dz4umW0R-zNcoTjH>pYF4pGrrWZR_dbcb@h;{;ERtA0iP{m>RfUI?KOTYRTEj z4+H;U1H}Bvvh0Ih`=@_hzxwz&rK$e+3E>b?Z>!Qq|D1r15-J37yX{@s{m(vKB@^O- z%xgK|XWG8Y5`Zdl_*u^FPh>~VzkRLAA>dR8)kSrpF`e~{Axsr==v@_NPpEk!56_OKVSdWcsP*Rj;lD)k$>|2Er25UJG+jrq**Lf}qGJh_hLYOb=x2 zt^qoO3SMEWRq0y2qq+$LVIfQyHfN2xe9XqnX30o>(874%B{;2ZS^TlC?oHnf{=T7E zrAg(}5~H**_XQ#3Luh_Z_vp-)HLqKq7}~Tj(f25gWNTj8Dpf#O6=q;xC6)hIqZ#-w XV{=09CkKgH00000NkvXXu0mjfs(+}K literal 0 HcmV?d00001 diff --git a/test/testdata/resources/assets_ios.xcassets/star_universal.imageset/Contents.json b/test/testdata/resources/assets_ios.xcassets/star_universal.imageset/Contents.json new file mode 100644 index 0000000000..e685f1fe58 --- /dev/null +++ b/test/testdata/resources/assets_ios.xcassets/star_universal.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "star.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "star_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "star_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/assets_ios.xcassets/star_universal.imageset/star.png b/test/testdata/resources/assets_ios.xcassets/star_universal.imageset/star.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca1b8a9f680cf9bb08e9e1145bfc13ce5c898c8 GIT binary patch literal 802 zcmV+-1Ks?IP)2!D1_kGom>NLPU zY<0AYr0s(ODlOO_>ls9sR=AaAoR*h5fw98-*clw)mXYy)P2i99UIQC)c+`M28ArYp zSyJtQRCrHT9_wLOkO@=nV$THrXwMb0;RI%R1#lD?-+$lg$P9OHdY7bcE zG4=*0u`R5D(xL@s+6F{*PFqd^ho?$4fQJWLYz|;m9R42{S|D*bq_;WQXY9~Lg{R7n0)me3;Tc zJn)}$8TMh{x=zs8(M2;29u?J$hNg>LQRNs|^A04Sb;|`)R8vs2e#M}@dR$Q@Dq1tb z-6}4;ngrs&>n|%9;)t2GU74pbGiN(|v3G@AkKdL+0H`3()sPsg7l`JqM)O(clVZst ziuxhLZz8P;)@DwEB_`mPkjG5Izp|u&xYZjr6Oc){1Fj^80%(^P9UCpP@QcQ>nQr(> z)Akn`i*M6h#u{uB?xt^Jum$zkQw@SXXoQMfKmT7qZ=5o{YgmL;byE+0?9@Y_x@0WA zD8?8mc5N;g&&1)1^@s607*qoM6N<$g05X*J^%m! literal 0 HcmV?d00001 diff --git a/test/testdata/resources/assets_ios.xcassets/star_universal.imageset/star_2x.png b/test/testdata/resources/assets_ios.xcassets/star_universal.imageset/star_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..78afe5cc223fbd7b7cd817f2bbbdb09b060a0daf GIT binary patch literal 1882 zcmV-g2c`IlP)hVhaZjft9I)F5En-J%i+ax1s8yIpqX`2A;gw!7Qe znb~$$Jk9K!%YXj=`~LHv%bb#EQOc*|&zMra!|FAnXQQd?yR9&#wL+Ir{CKK zxT}=+7lQyl7zM7g+L%soDNq84bz)B|c(;`>1jzBjc_eETB-aF%0ws{&pL`t1)=&g1 z&tkIzlJi{0ZDpBC+e|Zo5m4TjvR?sO29H;3<~-LSL9U-=RUv?%YBMuZm)oD%0(5s$ zSPE^4F+~^GaU07h*(nm!s|bt$0=(>ffOpcWMxjj1mYp8vnqE$(Y6Scg3ImDF@a*02 zu+(mzjQ#q)+{Q92tNPRnOrsnK0aKdF`*f>8LgOW~K|-67L9^Ybfu!&1851b%>)Z&R z-ou_H7w_AMScYY}5BIg}r~7VtHIRd==jV-Lf}~6!SP~>;3O-M0oh3obrcPcbVDf}6 z%+U~U5gCk08eYsuqD)k8&rr|NF{eI_>KqBk&*F1(<1#^}u7DzF6Ht(3pori3^~$JS zNNwE&3BA6hK^e@=A219>wawKE5sfi1RF04!MNDc&CN_&Q^l-qFn|!0b0LXRZh=dUmlKPO@i&+(- zxd6Kg;~H+(YJl=^5V4cyTF0nr&2j?!;4#GOMFba736b)&7VFCnN=-hAiXu126QSxp z|EoM>D}ygGX2y=OvQiIAcckG9l6H&t1MO*p*AYl%{^QRcR$m9F^u}E+BOUBEoh5%RO_jNreh4C?# z&JMycSX+6`L5d~+gM_JI4z&sS4}2~N>z2MH+Bf`lwdE-UxG|qj-Y=zj5F}>PO(3!A zI*?%S#{m9}0)K@_+Z0WWd{+&g^Ns|#AvX|Thxzb1X8H}Z5aF~M;nYx?djyym{85C> zS*+(IE+OtW#(J|qI$3aqbT!?zR>%^^+dOfjDfDiP#Eef*PB5zKuuB_9+Jrc-3|$Zu z+R%|2{;5i$ChJOoC5wZJyRMbwS6G1;(0qi`8jOV7iL*w1lLeE6`5Qbx#^C8(TZkTN z@5%h?rp42rtRSSdt%s5`tk^t^eWGqhbTi<)4LVu31zwzCn_l2qw3itkgB-E}y zfb1!G0VMtiezRkpteb7HEw;%8ujGpWD+zBK8NxdJKId`z3_RI7*#_J4Yrr>wvclE; zrVh-sfK!!DWy7!frU+nl>t@BW*`u=$G`3FLhL0{v#5V!?NpgYuPT-|kT^0H@;ZIKK zRlcmG`Ly-nX&Zx@}u_z1!RW2#{0+L<{1-0{SeN<-s} zZvyy~vnKE?M5*7zqD_cXS+{M=j~U+!I*!=o5%7nI4`KFxq1}!f3mghPQ#m{CXEjN?!{K6y$pkK%0e7e2gK;2454Rik92I^2P@yq7fUbVhW73uD%W+d;Ph_^~BTXGchfcWsI)s&B zEzLs+UB2si&00004XF*Lt006O% z3;baP000YENkl8K2qp>&_uDsY2BzDQ)btN#f8H^AIY30OB9skI=@p zOrdI}ic+9RsDY>v1)@r+kO;3LP^3yx@gpdz015sFeiq7H;NZD>;ffv1yu{-6iAlJ=)5H3#0iq}5ev=cVQr+fT~vFI z&QM;2yh@-lR8~JH6(?R{apEWY;SmGMz&hTejSqOy0F~8wNyUj06!acWbDl~ws%bD0 zXv~Y!@S;QsDo#B4N%SbY6dBZnw)st{CP8?qWW4TK?s9e1lV1Jzi4laQ1183P-@~_K zlxpNX+SWibbiGI>wuVfMpyE{j$6@mxWp_yL(Hwb`NcY8_iY}eKrg0(!Den>TkhX-D z6s+AH!u7j`F=SPlijagZ(%G=gDrF`@P+=ra6t(*Lw3ZjTJL(Oupc< z1l$4=yBvs~14O9WwaePdx>=D4sG5g2l5J(U9y{n)(O z+z6$dlOBdbAFkKgQ{uHxFSwkPCmPV=W!TtrBM}T-ilh} zFNU(^tMh#hclt4L(cX~5n32P(@p`jx=yYW+W8JcHQ5sr@At?T7Gx;LCXn6&EL#jcN zvsYmod0pQ1I+nMkJ>BuT4Kw`1rwe9DUYO|pG6efkmGTCRHaEg;Td$dVvn&eY!!m)m zl_A|k*DSq+hFk#DVj!k>5xP1wS%Bv+qgs&1@_3x)3*)^=hYV;YECUrFfMtYBh9U&- zI>W08Sz9bIxk$u81;}zqw3q~u>Rq$uY1G^XU-=byd%y8cdBO@WAg}0JUnEpv4m_wY ztP?MLbaU{Vw=C80IwFre&kW$EEo*mu4Chim#mp3wIX76F;T^enqm9SHj!;)qaXNj;()l&?*hFNxWK+|Ve-izBXN%>^ajDK zT_ALYw3fKWVnt3nlNUYzfC2nic4X{6&L63oAk;LOwi2+0`hk%0v)XjN1Nha}L<~5K2InFa! zro84G00O>~9X}S^4^!;2ykeENJ(Kl_mE`bh*roqN@m>(lER!$ z=t4=xSnZ3yf}AV9nVeY%vZHyw+h~HvFWE#;XBWo%9>k3M+xnSrgeTpK(QUnvq{u8TN>rGZ;h`Tv#wr zaEsrU8J;;7R)r}SnIKY9ob0_z3igCY6847d+wk>OE@GM$C6cD?*fVhB%C?0|4!b)u zeExXU`uzqPL4#YdHHH}|Tn7Qb#HCm&BcSk$sJb;d3X?S!haA!z+Zv1r6X=Vc(J*=U zirKo(TJzYfn=lc!xRu5xh!(e4!npT+fQF;PPAiyo6Qc=SViL9!gZ%BU+gkwgp z115^OxIc!8MNs*|!kgg6Y6Ygwnqq0wX2pzwK_74uw(?BDn}S5q9Lpp}h}~YLhHHE8 z(n=Uu9A=EN5%9z!2#Zy}jmMoJqGiyH4Rve7ZzT~-LJ zg{`;%2!TrjM3kgc-GAB4+Of>|meyPf`6r}^O@bh{xOeYia&mD~80F#=Y z*%=3Irx~@nIE4(tis)2OCd_kWg7C|WLERIoYj-tjqZ?hS^F=3Y25g=$ft+Jq9qr%b z`_8>$=IL~Wumkes&ZDLB5^mPBd-a@)h<08BQRu`~XI}9ZU6x|P)Pr}p*~idX-wa=?Xsds$8J#Pj&Z{HuT}8%^mq_`(lnNAiDMsVVxK!bI*J zpyp9Dz4umW0R-zNcoTjH>pYF4pGrrWZR_dbcb@h;{;ERtA0iP{m>RfUI?KOTYRTEj z4+H;U1H}Bvvh0Ih`=@_hzxwz&rK$e+3E>b?Z>!Qq|D1r15-J37yX{@s{m(vKB@^O- z%xgK|XWG8Y5`Zdl_*u^FPh>~VzkRLAA>dR8)kSrpF`e~{Axsr==v@_NPpEk!56_OKVSdWcsP*Rj;lD)k$>|2Er25UJG+jrq**Lf}qGJh_hLYOb=x2 zt^qoO3SMEWRq0y2qq+$LVIfQyHfN2xe9XqnX30o>(874%B{;2ZS^TlC?oHnf{=T7E zrAg(}5~H**_XQ#3Luh_Z_vp-)HLqKq7}~Tj(f25gWNTj8Dpf#O6=q;xC6)hIqZ#-w XV{=09CkKgH00000NkvXXu0mjfs(+}K literal 0 HcmV?d00001 diff --git a/test/testdata/resources/assets_tvos.xcassets/Contents.json b/test/testdata/resources/assets_tvos.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/test/testdata/resources/assets_tvos.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/assets_tvos.xcassets/star.imageset/Contents.json b/test/testdata/resources/assets_tvos.xcassets/star.imageset/Contents.json new file mode 100644 index 0000000000..72e45b5613 --- /dev/null +++ b/test/testdata/resources/assets_tvos.xcassets/star.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "star.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/assets_tvos.xcassets/star.imageset/star.png b/test/testdata/resources/assets_tvos.xcassets/star.imageset/star.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca1b8a9f680cf9bb08e9e1145bfc13ce5c898c8 GIT binary patch literal 802 zcmV+-1Ks?IP)2!D1_kGom>NLPU zY<0AYr0s(ODlOO_>ls9sR=AaAoR*h5fw98-*clw)mXYy)P2i99UIQC)c+`M28ArYp zSyJtQRCrHT9_wLOkO@=nV$THrXwMb0;RI%R1#lD?-+$lg$P9OHdY7bcE zG4=*0u`R5D(xL@s+6F{*PFqd^ho?$4fQJWLYz|;m9R42{S|D*bq_;WQXY9~Lg{R7n0)me3;Tc zJn)}$8TMh{x=zs8(M2;29u?J$hNg>LQRNs|^A04Sb;|`)R8vs2e#M}@dR$Q@Dq1tb z-6}4;ngrs&>n|%9;)t2GU74pbGiN(|v3G@AkKdL+0H`3()sPsg7l`JqM)O(clVZst ziuxhLZz8P;)@DwEB_`mPkjG5Izp|u&xYZjr6Oc){1Fj^80%(^P9UCpP@QcQ>nQr(> z)Akn`i*M6h#u{uB?xt^Jum$zkQw@SXXoQMfKmT7qZ=5o{YgmL;byE+0?9@Y_x@0WA zD8?8mc5N;g&&1)1^@s607*qoM6N<$g05X*J^%m! literal 0 HcmV?d00001 diff --git a/test/testdata/resources/assets_watchos.xcassets/Contents.json b/test/testdata/resources/assets_watchos.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/test/testdata/resources/assets_watchos.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/assets_watchos.xcassets/star.imageset/Contents.json b/test/testdata/resources/assets_watchos.xcassets/star.imageset/Contents.json new file mode 100644 index 0000000000..3b433b4289 --- /dev/null +++ b/test/testdata/resources/assets_watchos.xcassets/star.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "watch", + "filename" : "star_2x.png", + "scale" : "2x", + "screen-width" : "<=145", + }, + { + "idiom" : "watch", + "filename" : "star_2x.png", + "scale" : "2x", + "screen-width" : ">145", + }, + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/assets_watchos.xcassets/star.imageset/star_2x.png b/test/testdata/resources/assets_watchos.xcassets/star.imageset/star_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..78afe5cc223fbd7b7cd817f2bbbdb09b060a0daf GIT binary patch literal 1882 zcmV-g2c`IlP)hVhaZjft9I)F5En-J%i+ax1s8yIpqX`2A;gw!7Qe znb~$$Jk9K!%YXj=`~LHv%bb#EQOc*|&zMra!|FAnXQQd?yR9&#wL+Ir{CKK zxT}=+7lQyl7zM7g+L%soDNq84bz)B|c(;`>1jzBjc_eETB-aF%0ws{&pL`t1)=&g1 z&tkIzlJi{0ZDpBC+e|Zo5m4TjvR?sO29H;3<~-LSL9U-=RUv?%YBMuZm)oD%0(5s$ zSPE^4F+~^GaU07h*(nm!s|bt$0=(>ffOpcWMxjj1mYp8vnqE$(Y6Scg3ImDF@a*02 zu+(mzjQ#q)+{Q92tNPRnOrsnK0aKdF`*f>8LgOW~K|-67L9^Ybfu!&1851b%>)Z&R z-ou_H7w_AMScYY}5BIg}r~7VtHIRd==jV-Lf}~6!SP~>;3O-M0oh3obrcPcbVDf}6 z%+U~U5gCk08eYsuqD)k8&rr|NF{eI_>KqBk&*F1(<1#^}u7DzF6Ht(3pori3^~$JS zNNwE&3BA6hK^e@=A219>wawKE5sfi1RF04!MNDc&CN_&Q^l-qFn|!0b0LXRZh=dUmlKPO@i&+(- zxd6Kg;~H+(YJl=^5V4cyTF0nr&2j?!;4#GOMFba736b)&7VFCnN=-hAiXu126QSxp z|EoM>D}ygGX2y=OvQiIAcckG9l6H&t1MO*p*AYl%{^QRcR$m9F^u}E+BOUBEoh5%RO_jNreh4C?# z&JMycSX+6`L5d~+gM_JI4z&sS4}2~N>z2MH+Bf`lwdE-UxG|qj-Y=zj5F}>PO(3!A zI*?%S#{m9}0)K@_+Z0WWd{+&g^Ns|#AvX|Thxzb1X8H}Z5aF~M;nYx?djyym{85C> zS*+(IE+OtW#(J|qI$3aqbT!?zR>%^^+dOfjDfDiP#Eef*PB5zKuuB_9+Jrc-3|$Zu z+R%|2{;5i$ChJOoC5wZJyRMbwS6G1;(0qi`8jOV7iL*w1lLeE6`5Qbx#^C8(TZkTN z@5%h?rp42rtRSSdt%s5`tk^t^eWGqhbTi<)4LVu31zwzCn_l2qw3itkgB-E}y zfb1!G0VMtiezRkpteb7HEw;%8ujGpWD+zBK8NxdJKId`z3_RI7*#_J4Yrr>wvclE; zrVh-sfK!!DWy7!frU+nl>t@BW*`u=$G`3FLhL0{v#5V!?NpgYuPT-|kT^0H@;ZIKK zRlcmG`Ly-nX&Zx@}u_z1!RW2#{0+L<{1-0{SeN<-s} zZvyy~vnKE?M5*7zqD_cXS+{M=j~U+!I*!=o5%7nI4`KFxq1}!f3mghPQ#m{CXEjN?!{K6y$pkK%0e7e2gK;2454Rik92I^2P@yq7fUbVhW73uD%W+d;Ph_^~BTXGchfcWsI)s&B zEzLs+UB + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/testdata/resources/it.lproj/view_ios.xib b/test/testdata/resources/it.lproj/view_ios.xib new file mode 100644 index 0000000000..18612356c4 --- /dev/null +++ b/test/testdata/resources/it.lproj/view_ios.xib @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/Contents.json b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/Contents.json new file mode 100644 index 0000000000..3c78cd22d5 --- /dev/null +++ b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/Contents.json @@ -0,0 +1,111 @@ +{ + "images": [ + { + "extent": "full-screen", + "filename": "launch_image_414x736pt_3x.png", + "idiom": "iphone", + "minimum-system-version": "8.0", + "orientation": "portrait", + "scale": "3x", + "size": "414x736", + "subtype": "736h" + }, + { + "extent": "full-screen", + "filename": "launch_image_736x414pt_3x.png", + "idiom": "iphone", + "minimum-system-version": "8.0", + "orientation": "landscape", + "scale": "3x", + "size": "736x414", + "subtype": "736h" + }, + { + "extent": "full-screen", + "filename": "launch_image_375x667pt_2x.png", + "idiom": "iphone", + "minimum-system-version": "8.0", + "orientation": "portrait", + "scale": "2x", + "size": "375x667", + "subtype": "667h" + }, + { + "extent": "full-screen", + "filename": "launch_image_320x480pt_2x.png", + "idiom": "iphone", + "minimum-system-version": "7.0", + "orientation": "portrait", + "scale": "2x", + "size": "320x480" + }, + { + "extent": "full-screen", + "filename": "launch_image_320x568pt_2x.png", + "idiom": "iphone", + "minimum-system-version": "7.0", + "orientation": "portrait", + "scale": "2x", + "size": "320x568", + "subtype": "retina4" + }, + { + "extent": "full-screen", + "filename": "launch_image_768x1024pt.png", + "idiom": "ipad", + "minimum-system-version": "7.0", + "orientation": "portrait", + "scale": "1x", + "size": "768x1024" + }, + { + "extent": "full-screen", + "filename": "launch_image_1024x768pt.png", + "idiom": "ipad", + "minimum-system-version": "7.0", + "orientation": "landscape", + "scale": "1x", + "size": "1024x768" + }, + { + "extent": "full-screen", + "filename": "launch_image_768x1024pt_2x.png", + "idiom": "ipad", + "minimum-system-version": "7.0", + "orientation": "portrait", + "scale": "2x", + "size": "768x1024" + }, + { + "extent": "full-screen", + "filename": "launch_image_1024x768pt_2x.png", + "idiom": "ipad", + "minimum-system-version": "7.0", + "orientation": "landscape", + "scale": "2x", + "size": "1024x768" + }, + { + "extent": "full-screen", + "filename": "launch_image_1366x1024pt_2x.png", + "idiom": "ipad", + "minimum-system-version": "9.0", + "orientation": "landscape", + "scale": "2x", + "size": "1366x1024" + }, + { + "extent": "full-screen", + "filename": "launch_image_1024x1366pt_2x.png", + "idiom": "ipad", + "minimum-system-version": "9.0", + "orientation": "portrait", + "scale": "2x", + "size": "1024x1366" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1024x1366pt_2x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1024x1366pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c90ce3ffea0bde7773f0d46244a81cb2fa329770 GIT binary patch literal 36938 zcmeI5Piz}S6vn5hLTzXz3I_tG#RaLAYVWQcC-Gvt4RK2%;S!~(nsTA*^*C8-dyRKf zI{`$23W*aH7pe-u0dYfcb`MJ^}V|#=-_R*euMDyjPOE*Nbw>5V*WbD2N<<~BD z_1$sCrjPhb%h7WAX~zp1DYqI_c&giI32(;oN4qW8JI^Dd!dLueA@TcXpC=5zT1cG9 zmaTGYmaqDA8*P4SS6Gw|<`K}`pG=T4wYTcZ%VF*^X72SV8#dec*%`;Q7CdUY1_&4^pb5kS=+I*No!i@ zk|a@{LpjPp)vv9S9O-1*Ub3@J+IFl-nuB-_=MWWCbtCt-vQS@=r|LPipxtnzqTg^= zxY=s1Qr zN~M-gPPn|1taz4}%w;OMjFp+Rs=O9MESG8cw$!D-Ta)$DOI^Lg)Fgac>Sx-%SnJ$6 z@sVeLr$dZLvu&t3KNJdGC(0H2jaRc%{DrN(n+NKCyehTacF1MT6%w08*I&l^tCWn1 zYu9l-xtNnsubq>GK`rXIZ9cOi zG;w>?uJUdT93CKV$~!>a-#Qhlck2voYIggK=H7f-S6{U-oDv%aUYyKkGuga3 z+&vtz;CFd_x#Wuq4y7*B*2Hu#YWUE|(6c0TDkDxa9xx6ihkBuEy2x!ko=w#&HVtCG zV)i#ITPM0-4tLoYT+>}nOw(ZyN<(V{mlhFq04_>HYXg@S5p@7AN<(V{mlhFq04_>H zYXg@S5p@7AN<(V{mlhFq04_>HYXg@S5p@7AN<(V{mlhFq04_>HYXg@S5p@7AN<(V{ zmlhFq04_>HYXg@S5p@7AN<(V{mlhFq04_>HYXg@S5p@7AN<(V{mlhFq04_>HYXg@S z5p@7AN<(V{mlhFq04_>HYXg@S5p@7AN<(V{mlhFq04_>HYXg@S5p@7AN<(V{mlhFq z04_>HYu^&r*x(gF-W2csb;JvPSO5Csrg%Bf@a7iFjBPy5*u~cw`}1$n{>0dt#n{j1 z7;~;L_E7Nlb6=fc?97$<(#%r#+h4EG9AF0f!n*eP58r+8&fJfmc5dvFFB-CE-`gFx z-*|Duj$Ky$Z026ovHM}w-d}}-BmfT3gCu~uP&xnyaF7IWzfd{=2XGJsu#ZwQ00(f8 z1h8#ZIsgZ7kOZ)8RyqI&aF7JBZB{w}2XGJsux(Z{00(f81h8#ZIsgZ7kOZ)8RyqI& zaF7JBZB{w}2XGJsux(Z{00(f81h8#ZIsgZ7kOZ)8RyqI&aF7K4f7|Bw3s1yP`i|cW zUViJ73HiTD*u_VC&;HKFKQi`)dN!Et+ba(AI00wd;fDXU_I#BmS)BQ$?h})8 n&3NUP7hn4DfH3e1U1<*!$;r#i|Gjzsy*HCw z{Q2gMcP=fzyi5qWw6WgUBIH>xm&k=@;G_84y$|8zqQ8DCB;>gl*maJ4cmE|q%5OTY z?P$CCu3>q-oN0RFHjShX-F4iW{Lc>$Wy!H?@(0DH*7Votp0j=! z(3^)hTGruht76Mn>&w-F0Reh6GNnPU>xRalCdY9Nu-ULGOR*%nU6WVYg3@+#Q(E%^ zDiw1@#nSS+RI23kVzE@pza`~0y`XBks;?+|#V8aEO_N41xxNfmHL!P#t;Y4ya`3Gt z??sVssA|98&-GVwUa+g`l}bg`@@hV>fI#Wg$L zepHiXwov*S-B-^~7jnZ9JD{izOkdS=nmWtLwo)H|Kj_A(*_KMXv`5`21V4SIpTFlt zUbyGYSu&lTyD-$G*-Y1-nQyN*bLlX;b_WoQkk0fBTL(T>w`l0?2Nu0{2X5u^thKX>Bd;D(Us6X*n+A z`bJfqb{bcF9FI24h}NMauPC~%j5i*IRng z$m@ny#xam`ItJ_@+l52 zs_Lv^) zpG=M(O>~|3RPxOrq-@XC0&*~Q264b7$6Qkp_QqPf_^M4DN z#7FM)*XZw2_oNM#go;)3gM{8r)HegVx(hwA*lthaPC8C6P*vG4K-ZZpMKU^Bp`)HX zUePK$*bL|f$jpqZpjXvnW8;+*{Why=cR$vb@*2EU*fH#YiCyvZwCk;tUGem^>)7*s z*NN)+(P~q}$cvr6$-!_F{wUD;m11$FSXHNor#&{E0qt%#9N57j>q}kB7e-#wQ!`y} zja^Gu%GeXlj69QSpe_Ui;D;g z1TIX2YYSXlL|7njVH#Xp;Nl{}0)Y$D;MxKg7ZDZ+T$l#e7Pz>Gut4C#G`P0F#YKb# z0vD#iwFNFNA}kQNFb%FPaB&e~fxv}naBYE$iwFw@E=+@K3tU`8SRim=8eCi8;v&KV zfeX{%+5#6B5f%tsmMT7+c7pB3r1uiZkED*Rb4X!P4aS>sG zz=dgWZGnr62nz%*OoM9+TwFw0AaG$CTwCDcBEkZJ3)A4*C*oQjKLJQxcZ+{Nu;-7r+1P(JveDyyCy+s}OGqE_fV~4B;Z@;etTq?m2jS9=506%OhX)}F>oKDU*4Lgzckgaisa>{@|PJ3oyI5(7)(`o0F0e@P%vzAkaHf1fB z-bWk0EZXB)Ih}G&Lza4moZ3X3F$-0`&A?_T-dWqvY;T^moHF!jx;Moc1BGnEbwV>{ zXwxACO4+vI%L!qRe%6LemZ5gyj9GBwwHeq9#al3JTHn*&JK~Ii`YixC!`_K&m!Tzw wkU?7SHhfvMpUzlLLzenPP7gkUk;jjh$hTiyc=XDb_hqKoxOSuQ;O+LkClfVrMF0Q* literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1024x768pt_2x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1024x768pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..28ff3b0db4dff780a8383b7b4dd00907a0821600 GIT binary patch literal 27212 zcmeI5O^n+_6vwAfRdk_TWy zfAePY4zc!~wX@G3S$JZB5OQR-QC}zIK4u;w_ua#q-rpx})*Ox+=MzHif0VxukgFdZ zC1m-8-`YqwnlIRH*vmU!*rxeGFJfv!$|nbr<6fkx(x#h!P|5xI)wP`BdzIYFR#R_A zHM-?Dc4K;O_iW4Ez37&_+{x-fd0?{uJ(@bopw|r&dr-+`acySvZY`%|lJsIFx56h> zHkxZnEsUvRuytJ-zH-$CW|d>FW3SiG z45wqCD!HvRjciTp_xt(&Vm^#FHKSB2X}YPIrpgp*vJ<4vKn;>(BFJ8xI!)Zzk5WGj z6du=Uhudi-m*W$SUc>w9MWcy=WXO(D)CNwZ8F^is<>a{|pJ+SoW|!u<8tu{^4bp`9 z88iK&tuPIft#Hniqv5#|vo&cpM|01--d=C!)Jb~!5<@UVI@2?0?L<^tr%AXSyY%!W zb}Pqb-F@nJ#)~r%iI2>^b~^CqxbYF;mZgGNp)$KyyB1R?4dYfAcB>-G*7gu8wOW={ z%JHV-`T^(qOj(&W}QXBUlS!?2cJwX{?ysfA9lpgM(4TWzn4I`=)C63CqwO zkhv^>v(xMxeQmOLZ=maEr&8p^3FUjPlH1#KqhoAzN^zfT@7j*b&$Ft_w*>Xd+C=a8 zvPPamH~+sbpokCL=g-mKqQR{;ln9ln<_Af_PTF^3y0Xb~;`VmCh&$;xJwaLH-vC{I zaw$abvT$+8yN{+;fg_E_@=w7XIF*$z&4UYdGg zdFVAgG}HC`*wwHWm!K;c8oG+*0$=OdYKmS~G_ZO_8x<^*3q9Hnci9-<(_Ib>!{H>5 zhSWw}QbbUIxF8Lwjku(Upa5|}8d4i^NfAK-;(|1!HsX>Zf&#<^X-I9vB}D`Uhzrt? z+K5Yv2nrAvq#?BtmlP2cATCHlY9lTwA}ByykcQMoTv9|(fVdzHsg1a#h@b#*K^jsU zaY+$D0pfx*q&DJ`B7y?M1!+ia#3e-p1&9mMklKh#iU^}|G^F+|aV?A=0i*$Y?yt`t z@G}k_KgAvnRNTgTlaSq~3Ay|hA%ERu?eBza>xA66K#2VzA&-UczxvG?LX4}c^_AA( zhd;hwd6+2VYyJAQUw{7egT`-P_J2IULJ{)H#|N|aTQ_USsVkzNtQ-;@2Ok&hLmmn^ z0px)BzzN{K5P5(ckOL=x-xneekOOjH1h9@0X@DG%11ErGv&aMFfE+jhESp6hAP3~Y z31HbQ@&Gv?2SxzPW|0QS0Xc92ST>71Kn}=(6Tq@rQ1I&YYXCgaXZ{EC@eD}qJ V|GazS^`E$G_4L{LXU|=D^Iu5cMDYLs literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1366x1024pt_2x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_1366x1024pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4bcec9e6e5ca12e2f90910b6b076d09eb1a7c9ac GIT binary patch literal 35688 zcmeI4O>7%g5XWBuK^myUm(+^W%7vJri*(i7S2z9dU+ukmgg zC#R@z0Et7zsX`o(xFR^g1(mi)95`_0&rZ!p3nT5jSq;*oW40A#^Ox?_9&7wUqVH<{)9+}Fx zkV&cr@j8U^qv!oHL2BlbC0~81O+fhkb|)@|?(dATLyx`0-qOHE1`SsNy%A zHLkasYbCuuNHlwKWzPilpe-(oSMnu&)G)D%W6hpxhLO{B`CPRk26Ddd*~JNKim&F4 z?4;!tvJ)##K3jAPUe=qiOfR2bExNAozRP@<>uTU`$b3m%dk^WFnD27^TH6=3&Z!d} zdG!xF#RxaMrkeFb5usZoaz*^c)$9^~XK=K8qVC64spYgoE_1G&=_XyTjP2})_ATFN`ccJ7XB!$OR^x@%Q zi+R7p>q}K%WN;|s(wfYRxnir~QzK0;6H{YiikgyBscEuYWLu9{GZ__4gXmZEUc)lD z(7kkc#6tg`9$Uhu1(xKt4@2Wsx@LXENQR~XxQm$7^I#piE~Z5WLGzRZ|?gR#TG zM{j+9p0S0m=c?0-ox6YBoqmpK&)ha{-}<%o)XnPIpKs>H`-t(ebH98r6+gZ6L@Kf8 zj&-H?jRUE8=Dn^W%UtN5e%6uiIm%;aOuXKh993}*$)SA*mte^dL=4%(P&ODty~IW# zwnprb46Ok}o5j!?Fti4uAmWJvtpT1P0Kx=^3KxP73 z1GEMr4A2^&H9%`13nE$rv<7GmWG0|BKx-hv0IdO91GEORAfh!uYk<~3W&&D+|6Oaa zboI|OPl}~kzJBZP^0&W?v12dGm6FGe>&d4$dA9efl?Qvjc?m_}NeJv9D7vA5oI#~X zV*&NCn-thSuxX~Tfcl8~i24{4Kq?>=kP1izK>!UB8lSH0kW3J&}pr=5^l3%>3rP zXJ6cpH#gq6l6xtK5W2Fy*4RR5@f4v;XO|br$o12^Uy`q9{I#7Bp=V#9KMUxy2UiiQ zTz6XA(RTAK-SWBx)AsgoVbJx7HA2SCfp1!eI1={ozT?*Ne|+;WFF1BB|E|`QoBj$u zaMn%&eCK4NWt|*aRXcyPo-+nI3DCunDGa(DH`E8Ud>mING98xkLTnNp*7D1ALSehP zDXe$_7PNvUTC%DLOVxs+X-iA$4MCNak|ZmVQWTY{UMlIbETo@&Jx3%Xu-p1p<90e7 z`Ky*ch$3H?q<+6&=obrKurDdqYE_a|NmWH+A%@3pWDZ0(yp{wR#%bWt3LHOjJXfG` z%{{Ld)$(~dQT9pq)%CN9+%RQFC`tp?rX%*XEqop@=sCE*V4VmAtjpEA|Y zKkyM2ynrkV z*NcWUZ5VjPxu&nXVPv`%UT@UNK*4crz1Ws3N~NlZ_ENbmmi9`rXsWm>+7(kPt8Gmy zwyhXqvkBeLg>HCOkJd{X+J4N?!2Mk4w*!atIQNRz{p9-GbpceWc2so;?2Ww}H)7CTL9rh4v- zJgem~PoPOXlfwcx_4q84QS!(@o>rvnab^6NvtoF{!e~oRI4}&yCxJAWGRMV41PwSY zNP{VJTuemJfa8KRm@>!3L<9{uE=YqZb6iYB(17EDG?+5S#Y6-RI4($oDRW#*M9_fa zf;5;i$Hhbh4LB}HgDG=dOhnLt!3L<9{u zE=YqZb6iYB(17EDG?+5S#Y6-RI4($oDRW#*M9_faf;5;i$Hhbh4LB}HgDG=dOhnLt z!3L<9{uE=YqZUx+I=dI1o-k(gU)|ch_b+9P Bkh=f? literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_320x568pt_2x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_320x568pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..10e78eb3d56fb95d0dacf06f09a828080dd5b6a3 GIT binary patch literal 18081 zcmeI4&2QsG6u_rtg_bURfdeZfgyj_yh;{stIBiUlRk!KV3R0>zXxkHY?MZ7jv5oCC zX;0O1KuB=e6GBKJ{s6?C!&XB430#mka6+s=AS4c)V8(Wwmo&-jqCIWKa+}wAALIGW zd(Y0n{b+OJ&8rJ9E+B-iuCLX$5Sl+j=#qbVo{aqcdvJ$*JmatJh6r7Gp8m|C&py9~ z(9-KpV<+0Fzin7vJ7?P7KF;;pKCwoqe6#19))9_`eSF}!74eU6?~8(CSHwGdU8(yk z_|RE94e<8qM#DNivP!miv$|0386-d(N2btgx7^U^Rm3>1L1a2Ci$ZJ?9aY3-I-#&r z-xOB701J9fmn=n7g~d`%)%C?i?S`N!YC%?1S-52MI8WVzez=DPWu7aYiHsZ^2`P1ZDtSV-ZC8<{=H4X-Cb`f+MFv;xPE9M2VK zTyx*+L={n_6JXRRsw8BUeNHoRyE18%|1e5WhKrk;nlin zIWFb;T3H@-8tnKW9qbk$IGNm(L!34$b+BQffp?KDq73{BG1 zhN>HyYUJ}U2Cy8BK`O{LBlDr6KwA*Uwv471w9TmMw9Nx7`|d$m9t;A@EU%0eujK{g zvT!wDmd6bPuQ=C?bvKMm*TUQbqxYLdQW6_irZ z+&4`I&G8^1q1+E=ukxGVW)5Mo+$6|rA*SsTl`6!nQ~*Dx*G&#D$J32c|;q0zx*WuB>< z^M5xmi4WW-&(S@^?zDnRLd8~-gM?l)>Y4#wJ|H>qaMhl~9d;a@pe)lnK+754iez+n zhx$2taYxIvu^A*A$d#G#E~sVs!q{Nu#D0hFYO535FKP-oWR?C-U7>o^b$7ZeRFAq2 zJl}PlsH&y2jSK@X+I^G8uuUEmaJ8uGc~ZZl!=oOXP7k+sY7VL3kmhBvKo%A|jcUev z?hZUvHD83TK$W^Cy9Ms*@n$BYJ^10%8z@+7TXlp^UqR^X1BCwhmwf(;P)9-N!5%`!rwF~^-Fxr*+vI)3Ki6x^ zjoy#H{h-bvboo2w{QfUL-~F(5>x<)i^T_?=652bb=h@s-k=sA zxy*(xO^A`4NY*|px*Kyt1u?Yvq*AhCnL!=z60^|>$STL&9r|m33r$qcN}CwIZO&jP znAQb(m)JXhZt_D`zl^4p*psc}T_SzRm{vtkyzU$Rb!5s{F?N?YfB6k${&nT2k5Row Q|B1}{>PGGBTYGo^15A4BrT_o{ literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_375x667pt_2x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_375x667pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1d43ac9374637053eb29333c2e04f31cc42bab75 GIT binary patch literal 19292 zcmeI4O>Epm6vwBfMJ1369QZg{TaZ9u*B`r1vexcaC7Y5+Y)R7v(i3&lLgR@=ZV8^JNrBkh$#!w>x`Z&;qVZUel+OSy)<7&jF!(vGYU4orv z=`@{C*yyYYOOA&HwW5{{Ns)zxdPP>%g$3nlL6PK|D9NH+Ez5PSR?{R&h(D#)46z!X zxv8zU&&1P_-J`oP*8B_|b$-pO4%9$GmvdMA>Fr4VJPL7r7x@~|E_Ue|GYt*FY@4iO4V zOJP+BPj+;}vMJZ68e%J?WAZtDSVk&2uz%W_#+?8=g+%9^Z}rA1PgAV}nyiP3RP zYjYpOP|Awjl~qlVwWzCxJAW zHpj(81RXdoNP}r}TuemJf#ZTSm^R17L(jmM9_ibf;5;m$Hhbh9XKvXgK2YIOhnLu(jmM9_ibf;5;m$Hhbh9XKvX zgK2YIOhnLuaPA}e?Lfg;S??fp@B~7aUqk5kKgsn6gmxu_e!Pf~_BKKfJ8!=H*t3h{QD+z*ppJ3V#~Tqg;%g}K+8@SenEY~!r_Nyx@V_b4nHhm%bjlJ@xwCpvXCjf gf9+qE-~Eilo3qyYdw;x3|NpDi<+JTipSk$zUmSjuegFUf literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_414x736pt_3x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_414x736pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..adedcbca6a16a6de4e11e1d5239bbb560d2e6e62 GIT binary patch literal 25562 zcmeI3&2Jl35Wrs%mD-?Qpb`iPY2^s1w6*ucc49BKs}Q$MBwW(eh*TWtX8oM3V(*&W zb?xL-!HElpo)F>y;=&1W=a5Rk6>+2LKY&0*aD)r+cJ26$9XoFq)shWkDarKh%)H&- z%$qmf?S8Ov$a%o_UIm)PH~YJ^OREwRYPh$9`&Vdx?|bJ#BX08CM2s?OuUt? zD3#VK-L=*ZTzc!^M%6snGxN2?mC{0?t1|~p>KjtG*|0smTTFz0btVVHazYAK{Jmmg zC74j!s%%KBj!UI%Dw{MFO_i4ODK(p2Ue+#4nxbZ8MU~ZbQqAj`jIJnB|C1;!Fsb0y z>iTB+T7NqBuVP}?_glIwcRHO^C!KQK9a+ui^RlAJnwDf5NpIiwjc(HR7JE*P{FJF@ zx>n1#99s(f8rx3WFD4SfM1xQNx|*%QM7Gyw$0*8Oqa~{;MIL8VGY2-Uw%Z6#t!B!! zL7UX}J!YqlwQKDsNDvIDNK6y#AO9&zrEmJOelSVk@-RW+&Qs;Z)ARb9;{l^n}U;H0NH z;-li!tolCip(Qo7s%CXf)wMkI0W?Q_uoYA@d?S(7O~Wr)O=E}3EqkXR z^Fg3F$SZx-X*e!BEm}$!3VWoRWnIyXiI5j$w=oj+abhC1)3joU3ScNuwMGEYcX8|^JA{xa4lBrj0VsN&OYuG zW855@YR&RkhVFyRW%(Ocvs?7t;nAZ54J)ilEyML_u;z-1qeV9;V}mLc*o13WH_V`( zmCRsC&{{zr8s(=ou(Hk9(&{57Fk7yw%Y&y#Zc4kHx1+^d_8{;$g^tXbjHrk>7vZk<) ztOmdPrtt8n>FtwE;o(tJZuzEd`6aDC+sH7m3aW3fFx+B|0xd0Nv*~O>9vvRF*s!{^ zu~oL%3ig7$RJ9D7E3_ItG}iPsH`O#H2Tj4DzG*L9U|SEP*&Ag|1M64hLBlc}=)rP$ z!UDghC+rxy!$}|wp^UhMh@b-Ef;5CO;u0c)3Wy8R5Xy*4hzKemE=WTtBQ7B#sDQX2 z4WW#Fy-Y4YGzuDiP327^Y{Bnm7{Ubu2 zckaLQ-8DikU05%#RJ)HJ|G09Fy_5K@^5F2NZ%&_GzhoS?@3Z$4@4dZvig~*ibOG`G!Z74bO)9t$(JgP;lI-#W1!oba`CHfl<&6rYjJa;}T`JOYnZy0IM`d;cod z7oJ$a;}sHY2glwqoFZ{-2YJEf(i{ShRl2bq#CCB0o;buxacl=?JH=<>n>@CI*bd^u z;;`EZ8}+F@yTrSLgmBDUni}v}dnLAmv%Od1!(v`PxD+eJu^pW46vuWj*a}9TN6ce} qPx00X-W|ldga3*DtasN1-zkKb^d;b8Y5Hm9X literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_736x414pt_3x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_736x414pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..beed90b20dfba5bb7d2b9a15d27d8368ec4aec00 GIT binary patch literal 25409 zcmeI3%WoS+9LJ{!i4qzvNKk=5D_4kzy52{;iM_HN1v@2?a2h2pBqzGrog}N+yUXsn zc5#ZwQ3MnG?*+I`+?wooP`I9DbG(|1$G?%zoxO zj~w>n^|d!=XP%!Sgv_q4)Het@|0N-h-#h;pYq@{?!YAzO$!KLeA>^rN_|G}=`4<-n zDPH!QTj^HgE!z#-dB+QPXuj8um^UHi#a`sN2Q*c7=$;=`a(~^qn^SzRl6%K$=#8jG z_x+Wlm~I}eHQl2Fx8&s(t25=E%@VX}>L|T-D@g2KB`5OQ%;wEnP7yBYK_$1u3o2WU zb)^=@RI&1w>guMUER^zwWi2e2mlabt3Yu_WlTb*2BwRcDx0j=f!bqkg4d zj{Q~1?WbvEYg)J4&3EVWVZ5gqrBX@LP0ch_=AkBsLF)9>Ai0z!ImuI}i5vS->W6{C z^Ex|WC#~diywKp&f39{kC=?`pc8sFdb0W>i>)NCs&mF`>owy~2=D8Ye(KZdzgvA*X zzIhLaldj9Sq$YH9c z(KIaEG;Ff~a{$lr9Bc)7PU<{R7U&D|c&@!0#%(99`fX>AYEiIP)`pG1bD%4`7Pi8e z4U1Oi%G#u65GAx`uLeo#1TI~zS6M^e_dQ#mFVcm%LP>QCo~0J3H?Qs#bgIr5ONK#R z!z#E10kP45;ipp9L$|}%OFy;ukf}lVsnl0vpY3%{3;6KSKWvE!X-<_|@e`)dL!exy zU$L8Q()UJNPdZw@*p(tDPAFen>Czt4#DlH&EbnOGc!9FUPk@#`Iu%*# z=nS1`_VkRF`Nn25HL#I6Vit_DcB*YyIrHD*v)bwi{{>TLAI;#u`=PLVJaqf)P}n^l zI*hy)_-WPbR~u^vQGEB!Hik`hQJ~d%%bK&w+IaJL#Jb<3t*yGxR&c^~X_|VmA2r@J zF?4$vYM2E!Oi@sTg06mOHeKLY7qgkQvP%QIuV{k{%jiT8mct`9hR^hf6T@^^1k#Y& zh)aqHIv_4cLuw-~DI(~AxF8Lwjku(UpabH9G^94-k|Kf*hzrt?+K5Yv2s$7xNJDBP zE-51DfVdzHsg1a#h@b=Ff;6Ny;*uhQ4u}iVklKh#iU>L&E=WUaBQ7Z-=zzE&4XKT| zq==vc;(|1!HsX>Zf)0oa(vaGSONt0OATCHlY9lTwBItm)APuRFxTJ`n1LA@-q&DJ` zB7zQx3(}C{Q>SL;j7 z-n~D6S(+sZ`9c5g?r*i9Z@>Nhw|9Phn!jjB$ctC`dx|W@v$viQhVs+>bHez@=!tK# zbKxan+`Nb`3E;t6vwB_YUu`wkT|g7WMzc}0>>YTo5XUml%z`|ky5om+n%UvPg>K&Hny`# zd#Zp7H&{3z#FYaFIB`HiXjcM`TvvhTgU4y8zEQqoo%RKSm{UbZj2fXcHk3~*(>ex3t%MC2Y=0aV4 z&*=q~Y&J|Z{3O@ab%%*;KVe5G3Ip90q?{;B3o?yijN9`%(b7yqz#ZJhcHomZX=`AjZ%sq9z-R{)Xez1C*AV`o-jr3bbE*3Vi@ANzauihq?a(&v> z2UdHuIb%xU%*?gpfi)v6oQcDtQiyj*BiE|dJgf(f*K(XrHLkLaQ-oZt7F8AZTvInJ zJLLMjCZvN#J3i9Gbv?jUQjyENB=K^oC5fscsglBrB~q6_B=$^cG#%4w9{~-Sm!+1Z zsB&5Zc&0T-2bp@H&!qx=fsSdYZO7~CLDlN&`&e-8eN7k*0?%PxsWqqLcw||)n%9JB z!yqcEHML>;fo>bPQLmDLoMoA6+ic^#a%q_t6|=|}iiILy!ucYPr9DH+n?_qIE=LfX zO&C5QU3ZLL*e{8+c}{5%J|X>@XOXi`?*N~0_2*47MP?$^n&p!UJpz?W>Nh&gw(wiy zqo)%cD>{{2-SctSbCv9A(+!WY;VBiyM7^u(MtGi8jj$!KsR?7Fqh$>vlQ8R|8yM?@ z@Yy-~2iQKVp<=1XYqp5*w1d9x;gx+-6Z3U@tUDf3&nj{Ud8UG4ND|Dr6Ck06ErCZTXT9lCRND4b4*jv`;Tt)MC= z*;2zGDm;DTgJFw2DB$X{qU04#NDrqYHmm{eY}YN)!G2hm#pO~Gl%AOyxigBCM7aP% z!Bi3&?-sbLN1GXsl4l0;xFQUnSH|~w*bNWa7+upt7KU;x38X=_87?X!=)iD68dRI% zq9TG03>Tz9wHYodBIv+yK^jz>;i4jf4h$EhLA4n!DkA8>a6uYWo8h7&f({H9q(QYA zE-E7Ez;HntRGZTz9wHYodBIv+yK^jz> z;i4jf4h$EhLA4n!DkA8>a6uYWo8h7&f({H9q(QYAE-E7Ez;HntRGZTz9wHYodBIv+yK^j#1OkA1K8-Um*ul@DO`+YyI*qFQ< z$Qf&!O@xl0Md;)mg#P@Sd_O>_CnEIQE<)dIq-|;nMN$sus0tu(v9byU{`;%^cZ>fH>rqfw;Ss`m zG<{^V9!=l9SdUKJ|59BvZ_P~T%+-IaM_G^3?&PdTCwhzZ=)@0?i=8C2+M5n~-ZODr lojvQOe<@D?q4)KhkD{;YnZG{5+-Hb<8msH|FJIn$`yU6uGYtR$ literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_768x1024pt_2x.png b/test/testdata/resources/launch_images_ios.xcassets/launch_image.launchimage/launch_image_768x1024pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3f9f49a4486e6b76851a0f7aeaa9b0b211d2a46e GIT binary patch literal 28357 zcmeI5O>7%Q6vro}iqfE>LJxdMNGn$e5ViNi-Z);_Zi3yCNH|1^5ve%P&F&t^$LX}F5*(hT?!KLQ z@6GOS-pu|FS@pfumDf(sJ~vAUIla78T_a?gnKNYO1Uq>*J(HbI`Ae5WLLNWM|4osP z_MahS{=8G)h&F1k8g@UYTvZke(M>^bfBeR!w!*fZJ!#q_QT7lz7 zj^|1|ues@UqEafw2O9kP&(-z^1G!~z<#}3Tqq*fb@J@J0q?TJH&(Z#C_K_BVF$gsZaQ+bVsUMH~V z#jEU5&P{sy$Y~5GXCxEvIriRZ&p9TH_atF)DI_ygWY20;0%}HHQ1`r6IjOSMLxfVL z5?7Vz}kR%jETFw^bNzEWCt~F!X4I|UF=yJ8p8ZwS!8`NrOdLhfux7q^^2ahp(4@YWpEmgYYA%-v}JG*O@Kg!|(oKhnSG&$WTj8 z$SQOfR4%LEcsE<8JEN_K9W5u`m3%V@DPMD?)ZwBVY-596ipRuj*Dx)asM$0rHBM$|O}y4Ykj@nEYx$vYZ2K0s0C zZ-ACFIu%Ll=nNg!?9mx5@{P@4)xhq|jAubD%17FUBPaeFd{$eX*gvl+>?f=I*bjx> z%4{*N+hVhJ>tYL z9S#C%2yMhAL zgo+3i5h@~7M5u^R5uqYNMTCk76%i^TR79wVP!XXbLPdm%2o(`3B2+}Eh)@xsB0@!k zis(O5M7Nt?KVdWaWcu#gKiqlceQy4AlK8n?+ F{{w7G(b50_ literal 0 HcmV?d00001 diff --git a/test/testdata/resources/launch_screen_ios.storyboard b/test/testdata/resources/launch_screen_ios.storyboard new file mode 100644 index 0000000000..f4fc7f7736 --- /dev/null +++ b/test/testdata/resources/launch_screen_ios.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/testdata/resources/nonlocalized.strings b/test/testdata/resources/nonlocalized.strings new file mode 100644 index 0000000000..42909abc5f --- /dev/null +++ b/test/testdata/resources/nonlocalized.strings @@ -0,0 +1 @@ +"nonlocalized_string" = "I'm the same in every language!"; diff --git a/test/testdata/resources/nonlocalized_resource.txt b/test/testdata/resources/nonlocalized_resource.txt new file mode 100644 index 0000000000..935a287f5c --- /dev/null +++ b/test/testdata/resources/nonlocalized_resource.txt @@ -0,0 +1,2 @@ +This file should appear in the app bundle's root because it is included in +the resources attribute, not structured_resources. diff --git a/test/testdata/resources/settings_ios.bundle/Root.plist b/test/testdata/resources/settings_ios.bundle/Root.plist new file mode 100644 index 0000000000..bf054c0d7f --- /dev/null +++ b/test/testdata/resources/settings_ios.bundle/Root.plist @@ -0,0 +1,25 @@ + + + + + PreferenceSpecifiers + + + Title + Section Title + Type + PSGroupSpecifier + FooterText + Section Footer + + + Title + Foo + Type + PSToggleSwitchSpecifier + + + StringsTable + Root + + diff --git a/test/testdata/resources/settings_ios.bundle/it.lproj/Root.strings b/test/testdata/resources/settings_ios.bundle/it.lproj/Root.strings new file mode 100644 index 0000000000..e29018f971 --- /dev/null +++ b/test/testdata/resources/settings_ios.bundle/it.lproj/Root.strings @@ -0,0 +1,3 @@ +"Section Title" = "Titolo della sezione"; +"Section Footer" = "Piè di pagina della sezione"; +"Foo" = "Pippo"; diff --git a/test/testdata/resources/star.atlas/star.png b/test/testdata/resources/star.atlas/star.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca1b8a9f680cf9bb08e9e1145bfc13ce5c898c8 GIT binary patch literal 802 zcmV+-1Ks?IP)2!D1_kGom>NLPU zY<0AYr0s(ODlOO_>ls9sR=AaAoR*h5fw98-*clw)mXYy)P2i99UIQC)c+`M28ArYp zSyJtQRCrHT9_wLOkO@=nV$THrXwMb0;RI%R1#lD?-+$lg$P9OHdY7bcE zG4=*0u`R5D(xL@s+6F{*PFqd^ho?$4fQJWLYz|;m9R42{S|D*bq_;WQXY9~Lg{R7n0)me3;Tc zJn)}$8TMh{x=zs8(M2;29u?J$hNg>LQRNs|^A04Sb;|`)R8vs2e#M}@dR$Q@Dq1tb z-6}4;ngrs&>n|%9;)t2GU74pbGiN(|v3G@AkKdL+0H`3()sPsg7l`JqM)O(clVZst ziuxhLZz8P;)@DwEB_`mPkjG5Izp|u&xYZjr6Oc){1Fj^80%(^P9UCpP@QcQ>nQr(> z)Akn`i*M6h#u{uB?xt^Jum$zkQw@SXXoQMfKmT7qZ=5o{YgmL;byE+0?9@Y_x@0WA zD8?8mc5N;g&&1)1^@s607*qoM6N<$g05X*J^%m! literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/Contents.json b/test/testdata/resources/sticker_pack_ios.xcstickers/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/test/testdata/resources/sticker_pack_ios.xcstickers/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/Contents.json b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/Contents.json new file mode 100644 index 0000000000..967c5d958a --- /dev/null +++ b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/Contents.json @@ -0,0 +1,85 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "app_icon_29pt_2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "app_icon_29pt_3x.png", + "scale" : "3x" + }, + { + "size" : "60x45", + "idiom" : "iphone", + "filename" : "app_icon_60x45pt_2x.png", + "scale" : "2x" + }, + { + "size" : "60x45", + "idiom" : "iphone", + "filename" : "app_icon_60x45pt_3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "app_icon_29pt_2x.png", + "scale" : "2x" + }, + { + "size" : "67x50", + "idiom" : "ipad", + "filename" : "app_icon_67x50pt_2x.png", + "scale" : "2x" + }, + { + "size" : "74x55", + "idiom" : "ipad", + "filename" : "app_icon_74x55pt_2x.png", + "scale" : "2x" + }, + { + "size" : "27x20", + "idiom" : "universal", + "filename" : "app_icon_27x20pt_2x.png", + "scale" : "2x", + "platform" : "ios" + }, + { + "size" : "27x20", + "idiom" : "universal", + "filename" : "app_icon_27x20pt_3x.png", + "scale" : "3x", + "platform" : "ios" + }, + { + "size" : "32x24", + "idiom" : "universal", + "filename" : "app_icon_32x24pt_2x.png", + "scale" : "2x", + "platform" : "ios" + }, + { + "size" : "32x24", + "idiom" : "universal", + "filename" : "app_icon_32x24pt_3x.png", + "scale" : "3x", + "platform" : "ios" + }, + { + "size" : "1024x768", + "idiom" : "ios-marketing", + "filename" : "app_icon_1024x768pt.png", + "scale" : "1x", + "platform" : "ios" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_1024x768pt.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_1024x768pt.png new file mode 100644 index 0000000000000000000000000000000000000000..f417708b04f840fb710f4b62dbd65ea79f72f72e GIT binary patch literal 383 zcmeAS@N?(olHy`uVBq!ia0y~yU;#3j85o&?R6|L-ACO{6cl32+VA$Bt{U?zX$X7`A z2=ZlMs8R!}XklRZ1ycEffuYoZf#FpG1B2BJ1_tqhIlBUFfD&v0J|V9E85sWm|L-Vk zA_x>=O!9VjVf@dedk@HAFY)wsWq-)b$Dw3mx2(q#C{*U@;uunK>+L~CMj)?A;s5$P z4+jvFmf$^GGd~04Q--@CKwDHxTq8h=GNbv5A$b pk*Y) literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_27x20pt_2x.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_27x20pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b61c094312f476cdf6f97f408941d708a98c7768 GIT binary patch literal 278 zcmeAS@N?(olHy`uVBq!ia0vp^WD0J1{V& z#QAXoB~(jXBT7;dOH!?pi&B9UgOP!uxvqh+uAxbYfrXW^v6YdTu7SCgfx+4{?Rh8~ ca`RI%(<*UmxHHlI5>Nw!r>mdKI;Vst0M*+>o&W#< literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_27x20pt_3x.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_27x20pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f7acf5fb344e0644a1b36ddabb382407f220228b GIT binary patch literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^fk14-$P6S^w2yoQQY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fIo3((;}jPrZVt*x;TbtoKH@WU|pQ>gMoqR z0s~`T{10}Zlxm4VwlX%=H88g_FbL?I gn~S0$H$NpatrE8e#mlM}ff^V*UHx3vIVCg!04i-o`v3p{ literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_29pt_2x.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_29pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2c6fa235149d66efc4b1e2b6f192c7ce18fb3432 GIT binary patch literal 279 zcmeAS@N?(olHy`uVBq!ia0vp^Rv^sC3?y%t-pB@0Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xg+_0C{m5HFasE6@fg!4}{X;`*O~;s5{tjl&Jb7+6>t0g;)mfw`4|!IVXdBT+Qu b=BH$)RpQq0^uP2|paup{S3j3^P614Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>) zAW(!c$=lt9@jsL9Js^j@#M9T6{UI|ShpbHSS-yUtkdUW~V~EA+FVdQ&MBb@01~i5)Bpeg literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_32x24pt_2x.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_32x24pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8285be9ec8a622bfb4da5de1159b8eacf3499d54 GIT binary patch literal 279 zcmeAS@N?(olHy`uVBq!ia0vp^4nSl$lA7vyj0EPHGT^vIsE+;2QusU-Bm2Y5R zY%jPq4=AHr;u=wsl30>zm0Xkxq!^4049#^7jCBo7LJTadj7_WzjC2jmtqcr)R_(7u d(U6;;l9^VCTSJFB`%$0<22WQ%mvv4FO#ttYMRfoG literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_32x24pt_3x.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_32x24pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6c1602771cea2e7760bc4fe5f4976f91f84a1a2 GIT binary patch literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@$P6TnS0*U{DVB6cUq=Rpjs4tz5?O(Kg=CK) zUj~LMH3o);76yi2K%s^g3=E|P3=FRl7#OT(FffQ0%-I!a1C(G3@CkAK&%p5i|9?kW z6G5N|W0JSK3*&z#-FrX|dx@v7EBixcJ`N?RM+J>-Kp{a-7sn8d^T`Pktcw#wc*+zm0Xkxq!^4049#^7jCBo7LJTadj7_YJ%ybRRtqctO h1?GsOXvob^$xN%nt-;P$Wi?O(gQu&X%Q~loCIBiDMDhRt literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_60x45pt_2x.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_60x45pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e07d4e138cdb6374a59a889541e629668dae6298 GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^6+j%t$P6Ss&oQh4QY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fInGDGTmQ$fI=dkE{-7Vure^!H88g_ jFfcV!>_yR#o1c=IR*74KOZH4dpaup{S3j3^P6Nn{1`6_P!I zd>I(3)EF2VS{N990fib~Fff!FFfhDIU|_JC!N4G1FlSew4N!tDz$e7@KLf-6|Nk9j zO$31=j7i?^E{y+~bngK<>?NMQuIvw)`8eeG|HR2m0t!iax;Tb-9DjSzkP*l`wBW0M zE-(KAAafrN&k6>{2M4|QfgIHm*NBpo#FA92@J&OZ5N% literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_67x50pt_2x.png b/test/testdata/resources/sticker_pack_ios.xcstickers/app_icon.stickersiconset/app_icon_67x50pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..021381d833e0717a7b64eeaf4bd0f7bd6286c0d6 GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^Z9tsD$P6Sat#3sGDVB6cUq=Rpjs4tz5?O(Kg=CK) zUj~LMH3o);76yi2K%s^g3=E|P3=FRl7#OT(FffQ0%-I!a1C(G3@CkAK&%p5i|9?kW z6G5N|W0JSK3*&z#-FrX|dx@v7EBixcJ`Opv#nDq{0fodoT^vI!PA4Zwur5vz>1hfy z;BwOgs^nl`RJFfb2vnh3;u=wsl30>zm0Xkxq!^4049#^7jCBo7LJTadj18_;+ mtqcrqFI0&|(U6;;l9^VCTf?e9a~=XUFnGH9xvX z2m(bIlf2zs82>Zr-UD*jOFVsD*&j0VaVWAF2fYWH$RO$I;uzv_JUKyvb#a17Pg9@) zm)k)N2^j_kju{M$58CDeox-46;u=wsl30>zm0Xkxq!^4049#^7jCBo7LJTadjE$`f pOmq#*tqcq*$|hQ&Xvob^$xN%nt$|Zpvkj<$!PC{xWt~$(69B1$NcsQ( literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/Contents.json b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/Contents.json new file mode 100644 index 0000000000..dd06995b19 --- /dev/null +++ b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/Contents.json @@ -0,0 +1,17 @@ +{ + "stickers" : [ + { + "filename" : "sticker.sticker" + }, + { + "filename" : "sequence.stickersequence" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "grid-size" : "regular" + } +} \ No newline at end of file diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/Contents.json b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/Contents.json new file mode 100644 index 0000000000..c18ddf9cd3 --- /dev/null +++ b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/Contents.json @@ -0,0 +1,22 @@ +{ + "properties" : { + "duration" : 3, + "duration-type" : "fps", + "repetitions" : 0 + }, + "info" : { + "version" : 1, + "author" : "xcode" + }, + "frames" : [ + { + "filename" : "sequence_100pt_1.png" + }, + { + "filename" : "sequence_100pt_2.png" + }, + { + "filename" : "sequence_100pt_3.png" + } + ] +} \ No newline at end of file diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/sequence_100pt_1.png b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/sequence_100pt_1.png new file mode 100644 index 0000000000000000000000000000000000000000..bfac6f5baf8c9186236ae737befb309165074a41 GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^DIm14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>) zAW(!c$=lt9@jsL9Js^j@#M9T6{UI|ShrU5is+lZMNW{~{F~s6@a)Jcw;slYNra%XO z1_ss&21d=ooqvGxswJ)wB`Jv|saDBFsX&Us$iUEC*T7iU&?Lmb!pg+l%EUs~z}(8f kK-$Lv=u9LHx%nxXX_dG&WC{NG2-Lvf>FVdQ&MBb@0P~JTQ2+n{ literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/sequence_100pt_2.png b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sequence.stickersequence/sequence_100pt_2.png new file mode 100644 index 0000000000000000000000000000000000000000..d173fcc0e4df449b5778a1e8bbd098414162334c GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^DIm14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>k7JNtG5g+x4E978NlCnrd-E=~~XX$o}k zXJBBhU|`fN-1!G6uUg_7QIe8al4_M)lnSI6j0_CTbq$Pl4NXD}EUZk-txSPjb1MS_ i#dQW&C>nC}Q!>*kacc14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>~IqW5#zOL*KnfW*jSjBt(EC33Lc)B=-Se#BykYHV$Akxzm=-|)5 zz*@n;s9Cu44^UpU#5JNMC9x#cD!C{XNHG{07@F%E80#9Egcw*@nOImEnCcpsTNxPe i-zovQ4WS`7KP5A*61RrB_A_ok4Gf;HelF{r5}E+Jz(}P4 literal 0 HcmV?d00001 diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sticker.sticker/Contents.json b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sticker.sticker/Contents.json new file mode 100644 index 0000000000..879bc04b60 --- /dev/null +++ b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sticker.sticker/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "filename" : "sticker_100pt.png" + } +} \ No newline at end of file diff --git a/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sticker.sticker/sticker_100pt.png b/test/testdata/resources/sticker_pack_ios.xcstickers/sticker_pack.stickerpack/sticker.sticker/sticker_100pt.png new file mode 100644 index 0000000000000000000000000000000000000000..964fed057cf3a7e4d8a965d082ec0250477d62f3 GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^DIm14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>) zAW(!c$=lt9@jsL9Js^j@#M9T6{UI|Shpx(!&W1-oArVg(#}JFt$q5pyixWh8ngSjC z85meA7#KARcm4s&tCqM%l%yn-D`PWV19K|_ j16SuqCMX(m^HVa@DsgKtk&Eg8YGCkm^>bP0l+XkKy$ePD literal 0 HcmV?d00001 diff --git a/test/testdata/resources/storyboard_ios.storyboard b/test/testdata/resources/storyboard_ios.storyboard new file mode 100644 index 0000000000..c9da020190 --- /dev/null +++ b/test/testdata/resources/storyboard_ios.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/testdata/resources/structured/nested.txt b/test/testdata/resources/structured/nested.txt new file mode 100644 index 0000000000..b9c6a45ddc --- /dev/null +++ b/test/testdata/resources/structured/nested.txt @@ -0,0 +1,2 @@ +This file should be inside the directory "structured/nested.txt" inside the app +bundle because it was included using the structured_resources attribute. diff --git a/test/testdata/resources/unversioned_datamodel.xcdatamodel/contents b/test/testdata/resources/unversioned_datamodel.xcdatamodel/contents new file mode 100644 index 0000000000..b47c3e3021 --- /dev/null +++ b/test/testdata/resources/unversioned_datamodel.xcdatamodel/contents @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/testdata/resources/versioned_datamodel.xcdatamodeld/.xccurrentversion b/test/testdata/resources/versioned_datamodel.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000000..990524f5f5 --- /dev/null +++ b/test/testdata/resources/versioned_datamodel.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + v2.xcdatamodel + + diff --git a/test/testdata/resources/versioned_datamodel.xcdatamodeld/v1.xcdatamodel/contents b/test/testdata/resources/versioned_datamodel.xcdatamodeld/v1.xcdatamodel/contents new file mode 100644 index 0000000000..46e518da52 --- /dev/null +++ b/test/testdata/resources/versioned_datamodel.xcdatamodeld/v1.xcdatamodel/contents @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/testdata/resources/versioned_datamodel.xcdatamodeld/v2.xcdatamodel/contents b/test/testdata/resources/versioned_datamodel.xcdatamodeld/v2.xcdatamodel/contents new file mode 100644 index 0000000000..9589917b44 --- /dev/null +++ b/test/testdata/resources/versioned_datamodel.xcdatamodeld/v2.xcdatamodel/contents @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/testdata/resources/view_ios.xib b/test/testdata/resources/view_ios.xib new file mode 100644 index 0000000000..18612356c4 --- /dev/null +++ b/test/testdata/resources/view_ios.xib @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/tvos_application_test.sh b/test/tvos_application_test.sh new file mode 100755 index 0000000000..c9a49f4013 --- /dev/null +++ b/test/tvos_application_test.sh @@ -0,0 +1,215 @@ +#!/bin/bash + +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eu + +# Integration tests for bundling simple tvOS applications. + +function set_up() { + rm -rf app + mkdir -p app +} + +# Creates common source, targets, and basic plist for tvOS applications. +function create_common_files() { + cat > app/BUILD < app/main.m < app/Info.plist <> app/BUILD <> app/BUILD < app/entitlements.plist < + + + + test-an-entitlement + + + +EOF + + if is_device_build tvos ; then + # For device builds, we verify that the entitlements are in the codesign + # output. + create_dump_codesign "//app:app.ipa" "Payload/app.app" -d --entitlements :- + do_build tvos 10.0 //app:dump_codesign || fail "Should build" + + assert_contains "test-an-entitlement" \ + "test-genfiles/app/codesign_output" + else + # For simulator builds, the entitlements are added as a Mach-O section in + # the binary. + do_build tvos 10.0 //app:app || fail "Should build" + + print_debug_entitlements "test-bin/app/app.apple_binary_lipobin" | \ + assert_contains "test-an-entitlement" - + fi +} + +run_suite "tvos_application bundling tests" diff --git a/test/tvos_extension_test.sh b/test/tvos_extension_test.sh new file mode 100755 index 0000000000..ea385d1864 --- /dev/null +++ b/test/tvos_extension_test.sh @@ -0,0 +1,170 @@ +#!/bin/bash + +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eu + +# Integration tests for bundling tvOS apps with extensions. + +function set_up() { + rm -rf app + mkdir -p app +} + +# Creates common source, targets, and basic plist for tvOS applications. +function create_minimal_tvos_application_with_extension() { + cat > app/BUILD < app/main.m < app/Info-App.plist < app/Info-Ext.plist <$TEST_log || fail "foo failed"; +# expect_log "blah" "Expected to see 'blah' in output of 'foo'." +# } +# +# # Test that bar works. +# function test_bar() { +# bar 2>$TEST_log || fail "bar failed"; +# expect_not_log "ERROR" "Unexpected error from 'bar'." +# ... +# assert_equals $x $y +# } +# +# run_suite "Test suite for blah" +# ------------------------------------------------------------------------ +# +# Each test function is considered to pass iff fail() is not called +# while it is active. fail() may be called directly, or indirectly +# via other assertions such as expect_log(). run_suite must be called +# at the very end. +# +# A test function may redefine functions "set_up" and/or "tear_down"; +# these functions are executed before and after each test function, +# respectively. Similarly, "cleanup" and "timeout" may be redefined, +# and these function are called upon exit (of any kind) or a timeout. +# +# The user can pass --test_arg to bazel test to select specific tests +# to run. Specifying --test_arg multiple times allows to select several +# tests to be run in the given order. Additionally the user may define +# TESTS=(test_foo test_bar ...) to specify a subset of test functions to +# execute, for example, a working set during debugging. By default, all +# functions called test_* will be executed. +# +# This file provides utilities for assertions over the output of a +# command. The output of the command under test is directed to the +# file $TEST_log, and then the expect_log* assertions can be used to +# test for the presence of certain regular expressions in that file. +# +# The test framework is responsible for restoring the original working +# directory before each test. +# +# The order in which test functions are run is not defined, so it is +# important that tests clean up after themselves. +# +# Each test will be run in a new subshell. +# +# Functions named __* are not intended for use by clients. +# +# This framework implements the "test sharding protocol". +# + +[ -n "$BASH_VERSION" ] || + { echo "unittest.bash only works with bash!" >&2; exit 1; } + +DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +#### Configuration variables (may be overridden by testenv.sh or the suite): + +# This function may be called by testenv.sh or a test suite to enable errexit +# in a way that enables us to print pretty stack traces when something fails. +function enable_errexit() { + set -o errtrace + set -eu + trap __test_terminated_err ERR +} + +function disable_errexit() { + set +o errtrace + set +eu + trap - ERR +} + +#### Set up the test environment, branched from the old shell/testenv.sh + +# Enable errexit with pretty stack traces. +enable_errexit + +# Print message in "$1" then exit with status "$2" +die () { + # second argument is optional, defaulting to 1 + local status_code=${2:-1} + # Stop capturing stdout/stderr, and dump captured output + if [ "$CAPTURED_STD_ERR" -ne 0 -o "$CAPTURED_STD_OUT" -ne 0 ]; then + restore_outputs + if [ "$CAPTURED_STD_OUT" -ne 0 ]; then + cat "${TEST_TMPDIR}/captured.out" + CAPTURED_STD_OUT=0 + fi + if [ "$CAPTURED_STD_ERR" -ne 0 ]; then + cat "${TEST_TMPDIR}/captured.err" 1>&2 + CAPTURED_STD_ERR=0 + fi + fi + + if [ -n "${1-}" ] ; then + echo "$1" 1>&2 + fi + if [ -n "${BASH-}" ]; then + local caller_n=0 + while [ $caller_n -lt 4 ] && caller_out=$(caller $caller_n 2>/dev/null); do + test $caller_n -eq 0 && echo "CALLER stack (max 4):" + echo " $caller_out" + let caller_n=caller_n+1 + done 1>&2 + fi + if [ x"$status_code" != x -a x"$status_code" != x"0" ]; then + exit "$status_code" + else + exit 1 + fi +} + +# Print message in "$1" then record that a non-fatal error occurred in ERROR_COUNT +ERROR_COUNT="${ERROR_COUNT:-0}" +error () { + if [ -n "$1" ] ; then + echo "$1" 1>&2 + fi + ERROR_COUNT=$(($ERROR_COUNT + 1)) +} + +# Die if "$1" != "$2", print $3 as death reason +check_eq () { + [ "$1" = "$2" ] || die "Check failed: '$1' == '$2' ${3:+ ($3)}" +} + +# Die if "$1" == "$2", print $3 as death reason +check_ne () { + [ "$1" != "$2" ] || die "Check failed: '$1' != '$2' ${3:+ ($3)}" +} + +# The structure of the following if statements is such that if '[' fails +# (e.g., a non-number was passed in) then the check will fail. + +# Die if "$1" > "$2", print $3 as death reason +check_le () { + [ "$1" -gt "$2" ] || die "Check failed: '$1' <= '$2' ${3:+ ($3)}" +} + +# Die if "$1" >= "$2", print $3 as death reason +check_lt () { + [ "$1" -lt "$2" ] || die "Check failed: '$1' < '$2' ${3:+ ($3)}" +} + +# Die if "$1" < "$2", print $3 as death reason +check_ge () { + [ "$1" -ge "$2" ] || die "Check failed: '$1' >= '$2' ${3:+ ($3)}" +} + +# Die if "$1" <= "$2", print $3 as death reason +check_gt () { + [ "$1" -gt "$2" ] || die "Check failed: '$1' > '$2' ${3:+ ($3)}" +} + +# Die if $2 !~ $1; print $3 as death reason +check_match () +{ + expr match "$2" "$1" >/dev/null || \ + die "Check failed: '$2' does not match regex '$1' ${3:+ ($3)}" +} + +# Run command "$1" at exit. Like "trap" but multiple atexits don't +# overwrite each other. Will break if someone does call trap +# directly. So, don't do that. +ATEXIT="${ATEXIT-}" +atexit () { + if [ -z "$ATEXIT" ]; then + ATEXIT="$1" + else + ATEXIT="$1 ; $ATEXIT" + fi + trap "$ATEXIT" EXIT +} + +## TEST_TMPDIR +if [ -z "${TEST_TMPDIR:-}" ]; then + export TEST_TMPDIR="$(mktemp -d ${TMPDIR:-/tmp}/bazel-test.XXXXXXXX)" +fi +if [ ! -e "${TEST_TMPDIR}" ]; then + mkdir -p -m 0700 "${TEST_TMPDIR}" + # Clean TEST_TMPDIR on exit + atexit "rm -fr ${TEST_TMPDIR}" +fi + +# Functions to compare the actual output of a test to the expected +# (golden) output. +# +# Usage: +# capture_test_stdout +# ... do something ... +# diff_test_stdout "$TEST_SRCDIR/path/to/golden.out" + +# Redirect a file descriptor to a file. +CAPTURED_STD_OUT="${CAPTURED_STD_OUT:-0}" +CAPTURED_STD_ERR="${CAPTURED_STD_ERR:-0}" + +capture_test_stdout () { + exec 3>&1 # Save stdout as fd 3 + exec 4>"${TEST_TMPDIR}/captured.out" + exec 1>&4 + CAPTURED_STD_OUT=1 +} + +capture_test_stderr () { + exec 6>&2 # Save stderr as fd 6 + exec 7>"${TEST_TMPDIR}/captured.err" + exec 2>&7 + CAPTURED_STD_ERR=1 +} + +# Force XML_OUTPUT_FILE to an existing path +if [ -z "${XML_OUTPUT_FILE:-}" ]; then + XML_OUTPUT_FILE=${TEST_TMPDIR}/ouput.xml +fi + +#### Global variables: + +TEST_name="" # The name of the current test. + +TEST_log=$TEST_TMPDIR/log # The log file over which the + # expect_log* assertions work. Must + # be absolute to be robust against + # tests invoking 'cd'! + +TEST_passed="true" # The result of the current test; + # failed assertions cause this to + # become false. + +# These variables may be overridden by the test suite: + +TESTS=() # A subset or "working set" of test + # functions that should be run. By + # default, all tests called test_* are + # run. +if [ $# -gt 0 ]; then + # Legacy behavior is to ignore missing regexp, but with errexit + # the following line fails without || true. + # TODO(dmarting): maybe we should revisit the way of selecting + # test with that framework (use Bazel's environment variable instead). + TESTS=($(for i in $@; do echo $i; done | grep ^test_ || true)) + if (( ${#TESTS[@]} == 0 )); then + echo "WARNING: Arguments do not specifies tests!" >&2 + fi +fi + +TEST_verbose="true" # Whether or not to be verbose. A + # command; "true" or "false" are + # acceptable. The default is: true. + +TEST_script="$(pwd)/$0" # Full path to test script + +#### Internal functions + +function __show_log() { + echo "-- Test log: -----------------------------------------------------------" + [[ -e $TEST_log ]] && cat $TEST_log || echo "(Log file did not exist.)" + echo "------------------------------------------------------------------------" +} + +# Usage: __pad <pad-char> +# Print $title padded to 80 columns with $pad_char. +function __pad() { + local title=$1 + local pad=$2 + { + echo -n "$pad$pad $title " + printf "%80s" " " | tr ' ' "$pad" + } | head -c 80 + echo +} + +#### Exported functions + +# Usage: init_test ... +# Deprecated. Has no effect. +function init_test() { + : +} + + +# Usage: set_up +# Called before every test function. May be redefined by the test suite. +function set_up() { + : +} + +# Usage: tear_down +# Called after every test function. May be redefined by the test suite. +function tear_down() { + : +} + +# Usage: cleanup +# Called upon eventual exit of the test suite. May be redefined by +# the test suite. +function cleanup() { + : +} + +# Usage: timeout +# Called upon early exit from a test due to timeout. +function timeout() { + : +} + +# Usage: fail <message> [<message> ...] +# Print failure message with context information, and mark the test as +# a failure. The context includes a stacktrace including the longest sequence +# of calls outside this module. (We exclude the top and bottom portions of +# the stack because they just add noise.) Also prints the contents of +# $TEST_log. +function fail() { + __show_log >&2 + echo "$TEST_name FAILED:" "$@" "." >&2 + echo "$@" >$TEST_TMPDIR/__fail + TEST_passed="false" + __show_stack + # Cleanup as we are leaving the subshell now + tear_down + exit 1 +} + +# Usage: warn <message> +# Print a test warning with context information. +# The context includes a stacktrace including the longest sequence +# of calls outside this module. (We exclude the top and bottom portions of +# the stack because they just add noise.) +function warn() { + __show_log >&2 + echo "$TEST_name WARNING: $1." >&2 + __show_stack + + if [ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]; then + echo "$TEST_name WARNING: $1." >> "$TEST_WARNINGS_OUTPUT_FILE" + fi +} + +# Usage: show_stack +# Prints the portion of the stack that does not belong to this module, +# i.e. the user's code that called a failing assertion. Stack may not +# be available if Bash is reading commands from stdin; an error is +# printed in that case. +__show_stack() { + local i=0 + local trace_found=0 + + # Skip over active calls within this module: + while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} == ${BASH_SOURCE[0]} ]]; do + (( ++i )) + done + + # Show all calls until the next one within this module (typically run_suite): + while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} != ${BASH_SOURCE[0]} ]]; do + # Read online docs for BASH_LINENO to understand the strange offset. + # Undefined can occur in the BASH_SOURCE stack apparently when one exits from a subshell + echo "${BASH_SOURCE[i]:-"Unknown"}:${BASH_LINENO[i - 1]:-"Unknown"}: in call to ${FUNCNAME[i]:-"Unknown"}" >&2 + (( ++i )) + trace_found=1 + done + + [ $trace_found = 1 ] || echo "[Stack trace not available]" >&2 +} + +# Usage: expect_log <regexp> [error-message] +# Asserts that $TEST_log matches regexp. Prints the contents of +# $TEST_log and the specified (optional) error message otherwise, and +# returns non-zero. +function expect_log() { + local pattern=$1 + local message=${2:-Expected regexp "$pattern" not found} + grep -sq -- "$pattern" $TEST_log && return 0 + + fail "$message" + return 1 +} + +# Usage: expect_log_warn <regexp> [error-message] +# Warns if $TEST_log does not match regexp. Prints the contents of +# $TEST_log and the specified (optional) error message on mismatch. +function expect_log_warn() { + local pattern=$1 + local message=${2:-Expected regexp "$pattern" not found} + grep -sq -- "$pattern" $TEST_log && return 0 + + warn "$message" + return 1 +} + +# Usage: expect_log_once <regexp> [error-message] +# Asserts that $TEST_log contains one line matching <regexp>. +# Prints the contents of $TEST_log and the specified (optional) +# error message otherwise, and returns non-zero. +function expect_log_once() { + local pattern=$1 + local message=${2:-Expected regexp "$pattern" not found exactly once} + expect_log_n "$pattern" 1 "$message" +} + +# Usage: expect_log_n <regexp> <count> [error-message] +# Asserts that $TEST_log contains <count> lines matching <regexp>. +# Prints the contents of $TEST_log and the specified (optional) +# error message otherwise, and returns non-zero. +function expect_log_n() { + local pattern=$1 + local expectednum=${2:-1} + local message=${3:-Expected regexp "$pattern" not found exactly $expectednum times} + local count=$(grep -sc -- "$pattern" $TEST_log) + [[ $count = $expectednum ]] && return 0 + fail "$message" + return 1 +} + +# Usage: expect_not_log <regexp> [error-message] +# Asserts that $TEST_log does not match regexp. Prints the contents +# of $TEST_log and the specified (optional) error message otherwise, and +# returns non-zero. +function expect_not_log() { + local pattern=$1 + local message=${2:-Unexpected regexp "$pattern" found} + grep -sq -- "$pattern" $TEST_log || return 0 + + fail "$message" + return 1 +} + +# Usage: expect_log_with_timeout <regexp> <timeout> [error-message] +# Waits for the given regexp in the $TEST_log for up to timeout seconds. +# Prints the contents of $TEST_log and the specified (optional) +# error message otherwise, and returns non-zero. +function expect_log_with_timeout() { + local pattern=$1 + local timeout=$2 + local message=${3:-Regexp "$pattern" not found in "$timeout" seconds} + local count=0 + while [ $count -lt $timeout ]; do + grep -sq -- "$pattern" $TEST_log && return 0 + let count=count+1 + sleep 1 + done + + grep -sq -- "$pattern" $TEST_log && return 0 + fail "$message" + return 1 +} + +# Usage: expect_cmd_with_timeout <expected> <cmd> [timeout] +# Repeats the command once a second for up to timeout seconds (10s by default), +# until the output matches the expected value. Fails and returns 1 if +# the command does not return the expected value in the end. +function expect_cmd_with_timeout() { + local expected="$1" + local cmd="$2" + local timeout=${3:-10} + local count=0 + while [ $count -lt $timeout ]; do + local actual="$($cmd)" + [ "$expected" = "$actual" ] && return 0 + let count=count+1 + sleep 1 + done + + [ "$expected" = "$actual" ] && return 0 + fail "Expected '$expected' within ${timeout}s, was '$actual'" + return 1 +} + +# Usage: assert_one_of <expected_list>... <actual> +# Asserts that actual is one of the items in expected_list +# Example: assert_one_of ( "foo", "bar", "baz" ) actualval +function assert_one_of() { + local args=("$@") + local last_arg_index=$((${#args[@]} - 1)) + local actual=${args[last_arg_index]} + unset args[last_arg_index] + for expected_item in "${args[@]}"; do + [ "$expected_item" = "$actual" ] && return 0 + done; + + fail "Expected one of '${args[@]}', was '$actual'" + return 1 +} + +# Usage: assert_equals <expected> <actual> +# Asserts [ expected = actual ]. +function assert_equals() { + local expected=$1 actual=$2 + [ "$expected" = "$actual" ] && return 0 + + fail "Expected '$expected', was '$actual'" + return 1 +} + +# Usage: assert_not_equals <unexpected> <actual> +# Asserts [ unexpected != actual ]. +function assert_not_equals() { + local unexpected=$1 actual=$2 + [ "$unexpected" != "$actual" ] && return 0; + + fail "Expected not '$unexpected', was '$actual'" + return 1 +} + +# Usage: assert_contains <regexp> <file> [error-message] +# Asserts that file matches regexp. Prints the contents of +# file and the specified (optional) error message otherwise, and +# returns non-zero. +function assert_contains() { + local pattern=$1 + local file=$2 + local message=${3:-Expected regexp "$pattern" not found in "$file"} + grep -sq -- "$pattern" "$file" && return 0 + + cat "$file" >&2 + fail "$message" + return 1 +} + +# Usage: assert_not_contains <regexp> <file> [error-message] +# Asserts that file does not match regexp. Prints the contents of +# file and the specified (optional) error message otherwise, and +# returns non-zero. +function assert_not_contains() { + local pattern=$1 + local file=$2 + local message=${3:-Expected regexp "$pattern" found in "$file"} + grep -sq -- "$pattern" "$file" || return 0 + + cat "$file" >&2 + fail "$message" + return 1 +} + +# Updates the global variables TESTS if +# sharding is enabled, i.e. ($TEST_TOTAL_SHARDS > 0). +function __update_shards() { + [ -z "${TEST_TOTAL_SHARDS-}" ] && return 0 + + [ "$TEST_TOTAL_SHARDS" -gt 0 ] || + { echo "Invalid total shards $TEST_TOTAL_SHARDS" >&2; exit 1; } + + [ "$TEST_SHARD_INDEX" -lt 0 -o "$TEST_SHARD_INDEX" -ge "$TEST_TOTAL_SHARDS" ] && + { echo "Invalid shard $shard_index" >&2; exit 1; } + + TESTS=$(for test in "${TESTS[@]}"; do echo "$test"; done | + awk "NR % $TEST_TOTAL_SHARDS == $TEST_SHARD_INDEX") + + [ -z "${TEST_SHARD_STATUS_FILE-}" ] || touch "$TEST_SHARD_STATUS_FILE" +} + +# Usage: __test_terminated <signal-number> +# Handler that is called when the test terminated unexpectedly +function __test_terminated() { + __show_log >&2 + echo "$TEST_name FAILED: terminated by signal $1." >&2 + TEST_passed="false" + __show_stack + timeout + exit 1 +} + +# Usage: __test_terminated_err +# Handler that is called when the test terminated unexpectedly due to "errexit". +function __test_terminated_err() { + # When a subshell exits due to signal ERR, its parent shell also exits, + # thus the signal handler is called recursively and we print out the + # error message and stack trace multiple times. We're only interested + # in the first one though, as it contains the most information, so ignore + # all following. + if [[ -f $TEST_TMPDIR/__err_handled ]]; then + exit 1 + fi + __show_log >&2 + if [[ ! -z "$TEST_name" ]]; then + echo -n "$TEST_name " + fi + echo "FAILED: terminated because this command returned a non-zero status:" >&2 + touch $TEST_TMPDIR/__err_handled + TEST_passed="false" + __show_stack + # If $TEST_name is still empty, the test suite failed before we even started + # to run tests, so we shouldn't call tear_down. + if [[ ! -z "$TEST_name" ]]; then + tear_down + fi + exit 1 +} + +# Usage: __trap_with_arg <handler> <signals ...> +# Helper to install a trap handler for several signals preserving the signal +# number, so that the signal number is available to the trap handler. +function __trap_with_arg() { + func="$1" ; shift + for sig ; do + trap "$func $sig" "$sig" + done +} + +# Usage: <node> <block> +# Adds the block to the given node in the report file. Quotes in the in +# arguments need to be escaped. +function __log_to_test_report() { + local node="$1" + local block="$2" + if [[ ! -e "$XML_OUTPUT_FILE" ]]; then + local xml_header='<?xml version="1.0" encoding="UTF-8"?>' + echo "$xml_header<testsuites></testsuites>" > $XML_OUTPUT_FILE + fi + + # replace match on node with block and match + # replacement expression only needs escaping for quotes + perl -e "\ +\$input = @ARGV[0]; \ +\$/=undef; \ +open FILE, '+<$XML_OUTPUT_FILE'; \ +\$content = <FILE>; \ +if (\$content =~ /($node.*)\$/) { \ + seek FILE, 0, 0; \ + print FILE \$\` . \$input . \$1; \ +}; \ +close FILE" "$block" +} + +# Usage: <total> <passed> +# Adds the test summaries to the xml nodes. +function __finish_test_report() { + local total=$1 + local passed=$2 + local failed=$((total - passed)) + + cat $XML_OUTPUT_FILE | \ + sed \ + "s/<testsuites>/<testsuites tests=\"$total\" failures=\"0\" errors=\"$failed\">/" | \ + sed \ + "s/<testsuite>/<testsuite tests=\"$total\" failures=\"0\" errors=\"$failed\">/" \ + > $XML_OUTPUT_FILE.bak + + rm -f $XML_OUTPUT_FILE + mv $XML_OUTPUT_FILE.bak $XML_OUTPUT_FILE +} + +# Multi-platform timestamp function +UNAME=$(uname -s | tr 'A-Z' 'a-z') +if [ "$UNAME" = "linux" ] || [[ "$UNAME" =~ msys_nt* ]]; then + function timestamp() { + echo $(($(date +%s%N)/1000000)) + } +else + function timestamp() { + # OS X and FreeBSD do not have %N so python is the best we can do + python -c 'import time; print int(round(time.time() * 1000))' + } +fi + +function get_run_time() { + local ts_start=$1 + local ts_end=$2 + run_time_ms=$((${ts_end}-${ts_start})) + echo $(($run_time_ms/1000)).${run_time_ms: -3} +} + +# Usage: run_tests <suite-comment> +# Must be called from the end of the user's test suite. +# Calls exit with zero on success, non-zero otherwise. +function run_suite() { + echo >&2 + echo "$1" >&2 + echo >&2 + + __log_to_test_report "<\/testsuites>" "<testsuite></testsuite>" + + local total=0 + local passed=0 + + atexit "cleanup" + + # If the user didn't specify an explicit list of tests (e.g. a + # working set), use them all. + if [ ${#TESTS[@]} = 0 ]; then + TESTS=$(declare -F | awk '{print $3}' | grep ^test_) + elif [ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]; then + if grep -q "TESTS=" "$TEST_script" ; then + echo "TESTS variable overridden in Bazel sh_test. Please remove before submitting" \ + >> "$TEST_WARNINGS_OUTPUT_FILE" + fi + fi + + __update_shards + + for TEST_name in ${TESTS[@]}; do + >$TEST_log # Reset the log. + TEST_passed="true" + + total=$(($total + 1)) + if [[ "$TEST_verbose" == "true" ]]; then + __pad $TEST_name '*' >&2 + fi + + local run_time="0.0" + rm -f $TEST_TMPDIR/{__ts_start,__ts_end} + + if [ "$(type -t $TEST_name)" = function ]; then + # Save exit handlers eventually set. + local SAVED_ATEXIT="$ATEXIT"; + ATEXIT= + + # Run test in a subshell. + rm -f $TEST_TMPDIR/__err_handled + __trap_with_arg __test_terminated INT KILL PIPE TERM ABRT FPE ILL QUIT SEGV + ( + timestamp >$TEST_TMPDIR/__ts_start + set_up + eval $TEST_name + tear_down + timestamp >$TEST_TMPDIR/__ts_end + test $TEST_passed == "true" + ) 2>&1 | tee $TEST_TMPDIR/__log + # Note that tee will prevent the control flow continuing if the test + # spawned any processes which are still running and have not closed + # their stdout. + + test_subshell_status=${PIPESTATUS[0]} + if [ "$test_subshell_status" != 0 ]; then + TEST_passed="false" + # Ensure that an end time is recorded in case the test subshell + # terminated prematurely. + [ -f $TEST_TMPDIR/__ts_end ] || timestamp >$TEST_TMPDIR/__ts_end + fi + + # Calculate run time for the testcase. + local ts_start=$(cat $TEST_TMPDIR/__ts_start) + local ts_end=$(cat $TEST_TMPDIR/__ts_end) + run_time=$(get_run_time $ts_start $ts_end) + + # Eventually restore exit handlers. + if [ -n "$SAVED_ATEXIT" ]; then + ATEXIT="$SAVED_ATEXIT" + trap "$ATEXIT" EXIT + fi + else # Bad test explicitly specified in $TESTS. + fail "Not a function: '$TEST_name'" + fi + + local testcase_tag="" + + if [[ "$TEST_passed" == "true" ]]; then + if [[ "$TEST_verbose" == "true" ]]; then + echo "PASSED: $TEST_name" >&2 + fi + passed=$(($passed + 1)) + testcase_tag="<testcase name=\"$TEST_name\" status=\"run\" time=\"$run_time\" classname=\"\"></testcase>" + else + echo "FAILED: $TEST_name" >&2 + # end marker in CDATA cannot be escaped, we need to split the CDATA sections + log=$(cat $TEST_TMPDIR/__log | sed 's/]]>/]]>]]><![CDATA[/g') + fail_msg=$(cat $TEST_TMPDIR/__fail 2> /dev/null || echo "No failure message") + testcase_tag="<testcase name=\"$TEST_name\" status=\"run\" time=\"$run_time\" classname=\"\"><error message=\"$fail_msg\"><![CDATA[$log]]></error></testcase>" + fi + + if [[ "$TEST_verbose" == "true" ]]; then + echo >&2 + fi + __log_to_test_report "<\/testsuite>" "$testcase_tag" + done + + __finish_test_report $total $passed + __pad "$passed / $total tests passed." '*' >&2 + [ $total = $passed ] || { + __pad "There were errors." '*' + exit 1 + } >&2 + + exit 0 +} diff --git a/test/watchos_application_test.sh b/test/watchos_application_test.sh new file mode 100755 index 0000000000..931609b083 --- /dev/null +++ b/test/watchos_application_test.sh @@ -0,0 +1,262 @@ +#!/bin/bash + +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eu + +# Integration tests for bundling simple watchOS applications. + +function set_up() { + rm -rf app + mkdir -p app +} + +# Creates minimal watchOS application and extension targets along with a +# companion iOS app. +function create_minimal_watchos_application_with_companion() { + cat > app/BUILD <<EOF +load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application") +load("@build_bazel_rules_apple//apple:watchos.bzl", + "watchos_application", + "watchos_extension" + ) + +objc_library( + name = "lib", + srcs = ["main.m"], +) + +ios_application( + name = "phone_app", + bundle_id = "my.bundle.id", + families = ["iphone"], + infoplists = ["Info-PhoneApp.plist"], + provisioning_profile = "@build_bazel_rules_apple//test/testdata/provisioning:integration_testing.mobileprovision", + watch_application = ":watch_app", + deps = [":lib"], +) + +watchos_application( + name = "watch_app", + bundle_id = "my.bundle.id.watch_app", + extension = ":watch_ext", + infoplists = ["Info-WatchApp.plist"], + provisioning_profile = "@build_bazel_rules_apple//test/testdata/provisioning:integration_testing.mobileprovision", + deps = [":lib"], +) + +watchos_extension( + name = "watch_ext", + bundle_id = "my.bundle.id.watch_app.watch_ext", + infoplists = ["Info-WatchExt.plist"], + provisioning_profile = "@build_bazel_rules_apple//test/testdata/provisioning:integration_testing.mobileprovision", + deps = [":lib"], +) +EOF + + cat > app/main.m <<EOF +int main(int argc, char **argv) { + return 0; +} +EOF + + cat > app/Info-PhoneApp.plist <<EOF +{ + CFBundleIdentifier = "\${PRODUCT_BUNDLE_IDENTIFIER}"; + CFBundleName = "\${PRODUCT_NAME}"; + CFBundlePackageType = "APPL"; + CFBundleShortVersionString = "1"; + CFBundleSignature = "????"; +} +EOF + + cat > app/Info-WatchApp.plist <<EOF +{ + CFBundleIdentifier = "\${PRODUCT_BUNDLE_IDENTIFIER}"; + CFBundleName = "\${PRODUCT_NAME}"; + CFBundlePackageType = "APPL"; + CFBundleShortVersionString = "1"; + CFBundleSignature = "????"; + WKCompanionAppBundleIdentifier = "my.bundle.id"; + WKWatchKitApp = true; +} +EOF + + cat > app/Info-WatchExt.plist <<EOF +{ + CFBundleIdentifier = "\${PRODUCT_BUNDLE_IDENTIFIER}"; + CFBundleName = "\${PRODUCT_NAME}"; + CFBundlePackageType = "APPL"; + CFBundleShortVersionString = "1"; + CFBundleSignature = "????"; + NSExtension = { + NSExtensionAttributes = { + WKAppBundleIdentifier = "my.bundle.id.watch_app"; + }; + NSExtensionPointIdentifier = "com.apple.watchkit"; + }; +} +EOF +} + +# Asserts that the common OS and environment plist values in the watch +# application and extension have the correct values. +function assert_common_watch_app_and_extension_plist_values() { + assert_equals "2.0" "$(cat "test-genfiles/app/MinimumOSVersion")" + assert_equals "4" "$(cat "test-genfiles/app/UIDeviceFamily.0")" + + if is_device_build watchos ; then + assert_equals "WatchOS" \ + "$(cat "test-genfiles/app/CFBundleSupportedPlatforms.0")" + assert_equals "watchos" \ + "$(cat "test-genfiles/app/DTPlatformName")" + assert_contains "watchos.*" "test-genfiles/app/DTSDKName" + else + assert_equals "WatchSimulator" \ + "$(cat "test-genfiles/app/CFBundleSupportedPlatforms.0")" + assert_equals "watchsimulator" \ + "$(cat "test-genfiles/app/DTPlatformName")" + assert_contains "watchsimulator.*" "test-genfiles/app/DTSDKName" + fi + + # Verify the values injected by the environment_plist script. Some of these + # are dependent on the version of Xcode being used, and since we don't want to + # force a particular version to always be present, we just make sure that + # *something* is getting into the plist. + assert_not_equals "" "$(cat "test-genfiles/app/DTPlatformBuild")" + assert_not_equals "" "$(cat "test-genfiles/app/DTSDKBuild")" + assert_not_equals "" "$(cat "test-genfiles/app/DTPlatformVersion")" + assert_not_equals "" "$(cat "test-genfiles/app/DTXcode")" + assert_not_equals "" "$(cat "test-genfiles/app/DTXcodeBuild")" + assert_equals "com.apple.compilers.llvm.clang.1_0" \ + "$(cat "test-genfiles/app/DTCompiler")" + assert_not_equals "" "$(cat "test-genfiles/app/BuildMachineOSBuild")" +} + +# Tests that the Info.plist in the embedded watch application has the correct +# content. +function test_watch_app_plist_contents() { + create_minimal_watchos_application_with_companion + create_dump_plist "//app:phone_app.ipa" \ + "Payload/phone_app.app/Watch/watch_app.app/Info.plist" \ + BuildMachineOSBuild \ + CFBundleExecutable \ + CFBundleIdentifier \ + CFBundleName \ + CFBundleSupportedPlatforms:0 \ + DTCompiler \ + DTPlatformBuild \ + DTPlatformName \ + DTPlatformVersion \ + DTSDKBuild \ + DTSDKName \ + DTXcode \ + DTXcodeBuild \ + MinimumOSVersion \ + UIDeviceFamily:0 + do_build watchos 2.0 --watchos_minimum_os=2.0 //app:dump_plist \ + || fail "Should build" + + assert_equals "my.bundle.id.watch_app" "$(cat "test-genfiles/app/CFBundleIdentifier")" + assert_equals "watch_app" "$(cat "test-genfiles/app/CFBundleExecutable")" + assert_equals "watch_app" "$(cat "test-genfiles/app/CFBundleName")" + + assert_common_watch_app_and_extension_plist_values +} + +# Tests that the Info.plist in the embedded watch extension has the correct +# content. +function test_watch_ext_plist_contents() { + create_minimal_watchos_application_with_companion + create_dump_plist "//app:phone_app.ipa" \ + "Payload/phone_app.app/Watch/watch_app.app/PlugIns/watch_ext.appex/Info.plist" \ + BuildMachineOSBuild \ + CFBundleExecutable \ + CFBundleIdentifier \ + CFBundleName \ + CFBundleSupportedPlatforms:0 \ + DTCompiler \ + DTPlatformBuild \ + DTPlatformName \ + DTPlatformVersion \ + DTSDKBuild \ + DTSDKName \ + DTXcode \ + DTXcodeBuild \ + MinimumOSVersion \ + UIDeviceFamily:0 + do_build watchos 2.0 --watchos_minimum_os=2.0 //app:dump_plist \ + || fail "Should build" + + assert_equals "my.bundle.id.watch_app.watch_ext" "$(cat "test-genfiles/app/CFBundleIdentifier")" + assert_equals "watch_ext" "$(cat "test-genfiles/app/CFBundleExecutable")" + assert_equals "watch_ext" "$(cat "test-genfiles/app/CFBundleName")" + + assert_common_watch_app_and_extension_plist_values +} + +# Tests that the watch application is signed correctly. +function test_watch_application_is_signed() { + create_minimal_watchos_application_with_companion + create_dump_codesign "//app:phone_app.ipa" \ + "Payload/phone_app.app/Watch/watch_app.app" -vv + do_build watchos 2.0 //app:dump_codesign || fail "Should build" + + assert_contains "satisfies its Designated Requirement" \ + "test-genfiles/app/codesign_output" +} + +# Tests that the watch extension is signed correctly. +function test_watch_extension_is_signed() { + create_minimal_watchos_application_with_companion + create_dump_codesign "//app:phone_app.ipa" \ + "Payload/phone_app.app/Watch/watch_app.app/PlugIns/watch_ext.appex" -vv + do_build watchos 2.0 //app:dump_codesign || fail "Should build" + + assert_contains "satisfies its Designated Requirement" \ + "test-genfiles/app/codesign_output" +} + +# Tests that the provisioning profile is present when built for device. +function test_contains_provisioning_profile() { + # Ignore the test for simulator builds. + is_device_build watchos || return 0 + + create_minimal_watchos_application_with_companion + do_build watchos 2.0 //app:phone_app || fail "Should build" + + # Verify that the IPA contains the provisioning profile. + assert_zip_contains "test-bin/app/phone_app.ipa" \ + "Payload/phone_app.app/Watch/watch_app.app/PlugIns/watch_ext.appex/embedded.mobileprovision" +} + +# Tests that the watch application and IPA contain the WatchKit stub executable +# in the appropriate bundle and top-level support directories. +function test_contains_stub_executable() { + create_minimal_watchos_application_with_companion + do_build watchos 2.0 //app:phone_app || fail "Should build" + + # Verify that the IPA contains the provisioning profile. + assert_zip_contains "test-bin/app/phone_app.ipa" \ + "Payload/phone_app.app/Watch/watch_app.app/_WatchKitStub/WK" + + # Ignore the check for simulator builds. + is_device_build watchos || return 0 + + assert_zip_contains "test-bin/app/phone_app.ipa" \ + "WatchKitSupport2/WK" +} + +run_suite "watchos_application bundling tests"