Browse Source

Merge topic 'xcode_app_extensions'

eb5e33ba47 Xcode: Add support for embedding app extensions
f62a2bf44f Tests: Factor out XcodeProject-Embed check function findAttribute()

Acked-by: Kitware Robot <[email protected]>
Merge-request: !5934
Brad King 4 years ago
parent
commit
395e1d458e

+ 2 - 0
Help/manual/cmake-properties.7.rst

@@ -403,7 +403,9 @@ Properties on Targets
    /prop_tgt/XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY
    /prop_tgt/XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY
    /prop_tgt/XCODE_EMBED_type
+   /prop_tgt/XCODE_EMBED_type_CODE_SIGN_ON_COPY
    /prop_tgt/XCODE_EMBED_type_PATH
+   /prop_tgt/XCODE_EMBED_type_REMOVE_HEADERS_ON_COPY
    /prop_tgt/XCODE_EXPLICIT_FILE_TYPE
    /prop_tgt/XCODE_GENERATE_SCHEME
    /prop_tgt/XCODE_LINK_BUILD_PHASE_MODE

+ 5 - 0
Help/prop_tgt/XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY.rst

@@ -6,3 +6,8 @@ XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY
 Tell the :generator:`Xcode` generator to perform code signing for all the
 frameworks and libraries that are embedded using the
 :prop_tgt:`XCODE_EMBED_FRAMEWORKS <XCODE_EMBED_<type>>` property.
+
+.. versionadded:: 3.21
+
+This property was generalized to other types of embedded items.  See
+:prop_tgt:`XCODE_EMBED_<type>_CODE_SIGN_ON_COPY` for the more general form.

+ 6 - 0
Help/prop_tgt/XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY.rst

@@ -6,3 +6,9 @@ XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY
 Tell the :generator:`Xcode` generator to remove headers from all the
 frameworks that are embedded using the
 :prop_tgt:`XCODE_EMBED_FRAMEWORKS <XCODE_EMBED_<type>>` property.
+
+.. versionadded:: 3.21
+
+This property was generalized to other types of embedded items.  See
+:prop_tgt:`XCODE_EMBED_<type>_REMOVE_HEADERS_ON_COPY` for the more
+general form.

+ 15 - 5
Help/prop_tgt/XCODE_EMBED_type.rst

@@ -5,10 +5,20 @@ XCODE_EMBED_<type>
 
 Tell the :generator:`Xcode` generator to embed the specified list of items into
 the target bundle.  ``<type>`` specifies the embed build phase to use.
+See the Xcode documentation for the base location of each ``<type>``.
+
+The supported values for ``<type>`` are:
+
+``FRAMEWORKS``
+  The specified items will be added to the ``Embed Frameworks`` build phase.
+  The items can be CMake target names or paths to frameworks or libraries.
+
+``APP_EXTENSIONS``
+  .. versionadded:: 3.21
+
+  The specified items will be added to the ``Embed App Extensions`` build phase.
+  They must be CMake target names.
 
-Currently, the only supported value for ``<type>`` is ``FRAMEWORKS``.
-The specified items will be added to the ``Embed Frameworks`` build phase.
-The items can be CMake target names or paths to frameworks or libraries.
 See also :prop_tgt:`XCODE_EMBED_<type>_PATH`,
-:prop_tgt:`XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY` and
-:prop_tgt:`XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY`.
+:prop_tgt:`XCODE_EMBED_<type>_REMOVE_HEADERS_ON_COPY` and
+:prop_tgt:`XCODE_EMBED_<type>_CODE_SIGN_ON_COPY`.

+ 18 - 0
Help/prop_tgt/XCODE_EMBED_type_CODE_SIGN_ON_COPY.rst

@@ -0,0 +1,18 @@
+XCODE_EMBED_<type>_CODE_SIGN_ON_COPY
+------------------------------------
+
+.. versionadded:: 3.20
+
+Boolean property used only by the :generator:`Xcode` generator.  It specifies
+whether to perform code signing for the items that are embedded using the
+:prop_tgt:`XCODE_EMBED_<type>` property.
+
+The supported values for ``<type>`` are:
+
+``FRAMEWORKS``
+
+``APP_EXTENSIONS``
+  .. versionadded:: 3.21
+
+If a ``XCODE_EMBED_<type>_CODE_SIGN_ON_COPY`` property is not defined on the
+target, no code signing on copy will be performed for that ``<type>``.

+ 11 - 2
Help/prop_tgt/XCODE_EMBED_type_PATH.rst

@@ -3,7 +3,16 @@ XCODE_EMBED_<type>_PATH
 
 .. versionadded:: 3.20
 
-Tell the :generator:`Xcode` generator the relative path to use when embedding
-the items specified by :prop_tgt:`XCODE_EMBED_<type>`.  The path is relative
+This property is used only by the :generator:`Xcode` generator.  When defined,
+it specifies the relative path to use when embedding the items specified by
+:prop_tgt:`XCODE_EMBED_<type>`.  The path is relative
 to the base location of the ``Embed XXX`` build phase associated with
+``<type>``.  See the Xcode documentation for the base location of each
 ``<type>``.
+
+The supported values for ``<type>`` are:
+
+``FRAMEWORKS``
+
+``APP_EXTENSIONS``
+  .. versionadded:: 3.21

+ 20 - 0
Help/prop_tgt/XCODE_EMBED_type_REMOVE_HEADERS_ON_COPY.rst

@@ -0,0 +1,20 @@
+XCODE_EMBED_<type>_REMOVE_HEADERS_ON_COPY
+-----------------------------------------
+
+.. versionadded:: 3.20
+
+Boolean property used only by the :generator:`Xcode` generator.  It specifies
+whether to remove headers from all the frameworks that are embedded using the
+:prop_tgt:`XCODE_EMBED_<type>` property.
+
+The supported values for ``<type>`` are:
+
+``FRAMEWORKS``
+  If the ``XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY`` property is not
+  defined, headers will not be removed on copy by default.
+
+``APP_EXTENSIONS``
+  .. versionadded:: 3.21
+
+  If the ``XCODE_EMBED_APP_EXTENSIONS_REMOVE_HEADERS_ON_COPY`` property is not
+  defined, headers WILL be removed on copy by default.

+ 11 - 0
Help/release/dev/xcode_app_extensions.rst

@@ -0,0 +1,11 @@
+xcode_app_extensions
+--------------------
+
+* The :prop_tgt:`XCODE_EMBED_APP_EXTENSIONS <XCODE_EMBED_<type>>` target property
+  was added to tell the :generator:`Xcode` generator to embed app extensions
+  such as iMessage sticker packs.
+  Aspects of the embedding can be customized with the
+  :prop_tgt:`XCODE_EMBED_APP_EXTENSIONS_PATH <XCODE_EMBED_<type>>`,
+  :prop_tgt:`XCODE_EMBED_APP_EXTENSIONS_CODE_SIGN_ON_COPY <XCODE_EMBED_<type>_CODE_SIGN_ON_COPY>` and
+  :prop_tgt:`XCODE_EMBED_APP_EXTENSIONS_REMOVE_HEADERS_ON_COPY <XCODE_EMBED_<type>_REMOVE_HEADERS_ON_COPY>`
+  properties.

+ 51 - 20
Source/cmGlobalXCodeGenerator.cxx

@@ -3777,7 +3777,10 @@ void cmGlobalXCodeGenerator::AddDependAndLinkInformation(cmXCodeObject* target)
   }
 }
 
-void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
+void cmGlobalXCodeGenerator::AddEmbeddedObjects(
+  cmXCodeObject* target, const std::string& copyFilesBuildPhaseName,
+  const std::string& embedPropertyName, const std::string& dstSubfolderSpec,
+  int actionsOnByDefault)
 {
   cmGeneratorTarget* gt = target->GetTarget();
   if (!gt) {
@@ -3793,7 +3796,7 @@ void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
   if (!(isFrameworkTarget || isBundleTarget || isCFBundleTarget)) {
     return;
   }
-  cmProp files = gt->GetProperty("XCODE_EMBED_FRAMEWORKS");
+  cmProp files = gt->GetProperty(embedPropertyName);
   if (!files) {
     return;
   }
@@ -3801,16 +3804,15 @@ void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
   // Create an "Embedded Frameworks" build phase
   auto* copyFilesBuildPhase =
     this->CreateObject(cmXCodeObject::PBXCopyFilesBuildPhase);
-  std::string copyFilesBuildPhaseName = "Embed Frameworks";
-  std::string destinationFrameworks = "10";
   copyFilesBuildPhase->SetComment(copyFilesBuildPhaseName);
   copyFilesBuildPhase->AddAttribute("buildActionMask",
                                     this->CreateString("2147483647"));
   copyFilesBuildPhase->AddAttribute("dstSubfolderSpec",
-                                    this->CreateString(destinationFrameworks));
+                                    this->CreateString(dstSubfolderSpec));
   copyFilesBuildPhase->AddAttribute(
     "name", this->CreateString(copyFilesBuildPhaseName));
-  if (cmProp fwEmbedPath = gt->GetProperty("XCODE_EMBED_FRAMEWORKS_PATH")) {
+  if (cmProp fwEmbedPath =
+        gt->GetProperty(cmStrCat(embedPropertyName, "_PATH"))) {
     copyFilesBuildPhase->AddAttribute("dstPath",
                                       this->CreateString(*fwEmbedPath));
   } else {
@@ -3824,10 +3826,10 @@ void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
   for (std::string const& relFile : relFiles) {
     cmXCodeObject* buildFile{ nullptr };
     std::string filePath = relFile;
-    auto* genTarget = FindGeneratorTarget(relFile);
+    auto* genTarget = this->FindGeneratorTarget(relFile);
     if (genTarget) {
       // This is a target - get it's product path reference
-      auto* xcTarget = FindXCodeTarget(genTarget);
+      auto* xcTarget = this->FindXCodeTarget(genTarget);
       if (!xcTarget) {
         cmSystemTools::Error("Can not find a target for " +
                              genTarget->GetName());
@@ -3841,18 +3843,18 @@ void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
                              " is missing product reference");
         continue;
       }
-      auto it = FileRefToEmbedBuildFileMap.find(fileRefObject);
-      if (it == FileRefToEmbedBuildFileMap.end()) {
+      auto it = this->FileRefToEmbedBuildFileMap.find(fileRefObject);
+      if (it == this->FileRefToEmbedBuildFileMap.end()) {
         buildFile = this->CreateObject(cmXCodeObject::PBXBuildFile);
         buildFile->AddAttribute("fileRef", fileRefObject);
-        FileRefToEmbedBuildFileMap[fileRefObject] = buildFile;
+        this->FileRefToEmbedBuildFileMap[fileRefObject] = buildFile;
       } else {
         buildFile = it->second;
       }
     } else if (cmSystemTools::IsPathToFramework(relFile)) {
       // This is a regular string path - create file reference
-      auto it = EmbeddedLibRefs.find(relFile);
-      if (it == EmbeddedLibRefs.end()) {
+      auto it = this->EmbeddedLibRefs.find(relFile);
+      if (it == this->EmbeddedLibRefs.end()) {
         cmXCodeObject* fileRef =
           this->CreateXCodeFileReferenceFromPath(relFile, gt, "", nullptr);
         if (fileRef) {
@@ -3878,16 +3880,25 @@ void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
     cmXCodeObject* settings =
       this->CreateObject(cmXCodeObject::ATTRIBUTE_GROUP);
     cmXCodeObject* attrs = this->CreateObject(cmXCodeObject::OBJECT_LIST);
-    const auto& rmHeadersProp =
-      gt->GetSafeProperty("XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY");
-    if (cmIsOn(rmHeadersProp)) {
+
+    bool removeHeaders = actionsOnByDefault & RemoveHeadersOnCopyByDefault;
+    if (auto prop = gt->GetProperty(
+          cmStrCat(embedPropertyName, "_REMOVE_HEADERS_ON_COPY"))) {
+      removeHeaders = cmIsOn(*prop);
+    }
+    if (removeHeaders) {
       attrs->AddObject(this->CreateString("RemoveHeadersOnCopy"));
     }
-    const auto& codeSignProp =
-      gt->GetSafeProperty("XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY");
-    if (cmIsOn(codeSignProp)) {
+
+    bool codeSign = actionsOnByDefault & CodeSignOnCopyByDefault;
+    if (auto prop =
+          gt->GetProperty(cmStrCat(embedPropertyName, "_CODE_SIGN_ON_COPY"))) {
+      codeSign = cmIsOn(*prop);
+    }
+    if (codeSign) {
       attrs->AddObject(this->CreateString("CodeSignOnCopy"));
     }
+
     settings->AddAttributeIfNotEmpty("ATTRIBUTES", attrs);
     buildFile->AddAttributeIfNotEmpty("settings", settings);
     if (!buildFiles->HasObject(buildFile)) {
@@ -3896,11 +3907,30 @@ void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
   }
   copyFilesBuildPhase->AddAttribute("files", buildFiles);
   auto* buildPhases = target->GetAttribute("buildPhases");
-  // Insert embed build phase right before the post-build command
+  // Embed-something build phases must be inserted before the post-build
+  // command because that command is expected to be last
   buildPhases->InsertObject(buildPhases->GetObjectCount() - 1,
                             copyFilesBuildPhase);
 }
 
+void cmGlobalXCodeGenerator::AddEmbeddedFrameworks(cmXCodeObject* target)
+{
+  static const auto dstSubfolderSpec = "10";
+
+  this->AddEmbeddedObjects(target, "Embed Frameworks",
+                           "XCODE_EMBED_FRAMEWORKS", dstSubfolderSpec,
+                           NoActionOnCopyByDefault);
+}
+
+void cmGlobalXCodeGenerator::AddEmbeddedAppExtensions(cmXCodeObject* target)
+{
+  static const auto dstSubfolderSpec = "13";
+
+  this->AddEmbeddedObjects(target, "Embed App Extensions",
+                           "XCODE_EMBED_APP_EXTENSIONS", dstSubfolderSpec,
+                           RemoveHeadersOnCopyByDefault);
+}
+
 bool cmGlobalXCodeGenerator::CreateGroups(
   std::vector<cmLocalGenerator*>& generators)
 {
@@ -4280,6 +4310,7 @@ bool cmGlobalXCodeGenerator::CreateXCodeObjects(
   for (auto t : targets) {
     this->AddDependAndLinkInformation(t);
     this->AddEmbeddedFrameworks(t);
+    this->AddEmbeddedAppExtensions(t);
     // Inherit project-wide values for any target-specific search paths.
     this->InheritBuildSettingAttribute(t, "HEADER_SEARCH_PATHS");
     this->InheritBuildSettingAttribute(t, "SYSTEM_HEADER_SEARCH_PATHS");

+ 13 - 0
Source/cmGlobalXCodeGenerator.h

@@ -144,6 +144,13 @@ protected:
   }
 
 private:
+  enum EmbedActionFlags
+  {
+    NoActionOnCopyByDefault = 0,
+    CodeSignOnCopyByDefault = 1,
+    RemoveHeadersOnCopyByDefault = 2,
+  };
+
   bool ParseGeneratorToolset(std::string const& ts, cmMakefile* mf);
   bool ProcessGeneratorToolsetField(std::string const& key,
                                     std::string const& value, cmMakefile* mf);
@@ -208,7 +215,13 @@ private:
                                     const char* attribute);
   cmXCodeObject* CreateUtilityTarget(cmGeneratorTarget* gtgt);
   void AddDependAndLinkInformation(cmXCodeObject* target);
+  void AddEmbeddedObjects(cmXCodeObject* target,
+                          const std::string& copyFilesBuildPhaseName,
+                          const std::string& embedPropertyName,
+                          const std::string& dstSubfolderSpec,
+                          int actionsOnByDefault);
   void AddEmbeddedFrameworks(cmXCodeObject* target);
+  void AddEmbeddedAppExtensions(cmXCodeObject* target);
   void AddPositionIndependentLinkAttribute(cmGeneratorTarget* target,
                                            cmXCodeObject* buildSettings,
                                            const std::string& configName);

+ 1 - 1
Tests/RunCMake/CMakeLists.txt

@@ -554,7 +554,7 @@ endif()
 
 if(XCODE_VERSION)
   add_RunCMake_test(XcodeProject -DXCODE_VERSION=${XCODE_VERSION})
-  add_RunCMake_test(XcodeProject-Embed)
+  add_RunCMake_test(XcodeProject-Embed -DXCODE_VERSION=${XCODE_VERSION})
 
   # This test can take a very long time due to lots of combinations.
   # Use a long default timeout and provide an option to customize it.

+ 4 - 0
Tests/RunCMake/XcodeProject-Embed/EmbedAppExtensions-iOS-check.cmake

@@ -0,0 +1,4 @@
+include(${CMAKE_CURRENT_LIST_DIR}/findAttribute.cmake)
+
+findAttribute(${test} "RemoveHeadersOnCopy" TRUE)
+findAttribute(${test} "CodeSignOnCopy" FALSE)

+ 1 - 0
Tests/RunCMake/XcodeProject-Embed/EmbedAppExtensions-iOS.cmake

@@ -0,0 +1 @@
+include(${CMAKE_CURRENT_LIST_DIR}/EmbedAppExtensions.cmake)

+ 4 - 0
Tests/RunCMake/XcodeProject-Embed/EmbedAppExtensions-macOS-check.cmake

@@ -0,0 +1,4 @@
+include(${CMAKE_CURRENT_LIST_DIR}/findAttribute.cmake)
+
+findAttribute(${test} "RemoveHeadersOnCopy" TRUE)
+findAttribute(${test} "CodeSignOnCopy" FALSE)

+ 1 - 0
Tests/RunCMake/XcodeProject-Embed/EmbedAppExtensions-macOS.cmake

@@ -0,0 +1 @@
+include(${CMAKE_CURRENT_LIST_DIR}/EmbedAppExtensions.cmake)

+ 21 - 0
Tests/RunCMake/XcodeProject-Embed/EmbedAppExtensions.cmake

@@ -0,0 +1,21 @@
+add_library(app_extension MODULE Empty.txt)
+set_target_properties(app_extension PROPERTIES
+  LINKER_LANGUAGE CXX
+  BUNDLE YES
+  XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
+  XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ""
+  XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
+  MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in"
+  MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.app.app_extension"
+  XCODE_PRODUCT_TYPE "com.apple.product-type.app-extension"
+  XCODE_EXPLICIT_FILE_TYPE "wrapper.app-extension"
+)
+
+add_executable(app MACOSX_BUNDLE main.m)
+add_dependencies(app app_extension)
+set_target_properties(app PROPERTIES
+  XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO"
+  XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ""
+  XCODE_EMBED_APP_EXTENSIONS app_extension
+  MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.app"
+)

+ 3 - 13
Tests/RunCMake/XcodeProject-Embed/EmbedFrameworksFlagsOff-check.cmake

@@ -1,14 +1,4 @@
-function(findAttribute project attr)
-  execute_process(
-      COMMAND grep ${attr} ${RunCMake_TEST_BINARY_DIR}/${project}.xcodeproj/project.pbxproj
-      OUTPUT_VARIABLE output_var
-      RESULT_VARIABLE result_var
-  )
+include(${CMAKE_CURRENT_LIST_DIR}/findAttribute.cmake)
 
-  if(NOT result_var)
-    set(RunCMake_TEST_FAILED "${attr} attribute is set" PARENT_SCOPE)
-  endif()
-endfunction()
-
-findAttribute(${test} "RemoveHeadersOnCopy")
-findAttribute(${test} "CodeSignOnCopy")
+findAttribute(${test} "RemoveHeadersOnCopy" FALSE)
+findAttribute(${test} "CodeSignOnCopy" FALSE)

+ 3 - 13
Tests/RunCMake/XcodeProject-Embed/EmbedFrameworksFlagsOnNoSubdir-check.cmake

@@ -1,14 +1,4 @@
-function(findAttribute project attr)
-  execute_process(
-      COMMAND grep ${attr} ${RunCMake_TEST_BINARY_DIR}/${project}.xcodeproj/project.pbxproj
-      OUTPUT_VARIABLE output_var
-      RESULT_VARIABLE result_var
-  )
+include(${CMAKE_CURRENT_LIST_DIR}/findAttribute.cmake)
 
-  if(result_var)
-    set(RunCMake_TEST_FAILED "${attr} attribute not set" PARENT_SCOPE)
-  endif()
-endfunction()
-
-findAttribute(${test} "RemoveHeadersOnCopy")
-findAttribute(${test} "CodeSignOnCopy")
+findAttribute(${test} "RemoveHeadersOnCopy" TRUE)
+findAttribute(${test} "CodeSignOnCopy" TRUE)

+ 3 - 13
Tests/RunCMake/XcodeProject-Embed/EmbedFrameworksFlagsOnWithSubdir-check.cmake

@@ -1,14 +1,4 @@
-function(findAttribute project attr)
-  execute_process(
-      COMMAND grep ${attr} ${RunCMake_TEST_BINARY_DIR}/${project}.xcodeproj/project.pbxproj
-      OUTPUT_VARIABLE output_var
-      RESULT_VARIABLE result_var
-  )
+include(${CMAKE_CURRENT_LIST_DIR}/findAttribute.cmake)
 
-  if(result_var)
-    set(RunCMake_TEST_FAILED "${attr} attribute not set" PARENT_SCOPE)
-  endif()
-endfunction()
-
-findAttribute(${test} "RemoveHeadersOnCopy")
-findAttribute(${test} "CodeSignOnCopy")
+findAttribute(${test} "RemoveHeadersOnCopy" TRUE)
+findAttribute(${test} "CodeSignOnCopy" TRUE)

+ 0 - 0
Tests/RunCMake/XcodeProject-Embed/Empty.txt


+ 31 - 0
Tests/RunCMake/XcodeProject-Embed/Info.plist.in

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleDisplayName</key>
+    <string>SomeExtension</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+    <string>com.example.app.app_extension</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>app</string>
+	<key>CFBundlePackageType</key>
+	<string>XPC!</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0.0</string>
+	<key>CFBundleVersion</key>
+	<string>1.0.0</string>
+	<key>NSExtension</key>
+	<dict>
+		<key>NSExtensionPointIdentifier</key>
+        <string>com.apple.widgetkit-extension</string>
+		<key>NSExtensionPrincipalClass</key>
+        <string>SomeExtensionBrowserViewController</string>
+	</dict>
+</dict>
+</plist>

+ 31 - 0
Tests/RunCMake/XcodeProject-Embed/RunCMakeTest.cmake

@@ -41,3 +41,34 @@ endfunction()
 
 TestFlagsOn(EmbedFrameworksFlagsOnNoSubdir)
 TestFlagsOn(EmbedFrameworksFlagsOnWithSubdir)
+
+
+function(TestAppExtension platform)
+  set(testName EmbedAppExtensions-${platform})
+  if(NOT platform STREQUAL "macOS")
+    set(RunCMake_TEST_OPTIONS -DCMAKE_SYSTEM_NAME=${platform})
+  endif()
+  set(RunCMake_TEST_NO_CLEAN 1)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${testName}-build)
+
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+
+  run_cmake(${testName})
+  run_cmake_command(${testName}-build
+    ${CMAKE_COMMAND} --build ${RunCMake_TEST_BINARY_DIR}
+                     --config Debug
+                     --target app
+  )
+endfunction()
+
+# Isolate device tests from host architecture selection.
+unset(ENV{CMAKE_OSX_ARCHITECTURES})
+
+if(XCODE_VERSION VERSION_GREATER_EQUAL 8)
+  # The various flag on/off combinations are tested by the EmbedFrameworks...
+  # tests, so we don't duplicate all the combinations here. We only verify the
+  # defaults, which is to remove headers on copy, but not code sign.
+  TestAppExtension(macOS)
+  TestAppExtension(iOS)
+endif()

+ 19 - 0
Tests/RunCMake/XcodeProject-Embed/findAttribute.cmake

@@ -0,0 +1,19 @@
+cmake_policy(VERSION 3.1...3.20)
+
+function(findAttribute project attr expectPresent)
+  execute_process(
+      COMMAND grep ${attr} ${RunCMake_TEST_BINARY_DIR}/${project}.xcodeproj/project.pbxproj
+      OUTPUT_VARIABLE output_var
+      RESULT_VARIABLE result_var
+  )
+
+  if(${expectPresent})
+    if(result_var)
+      set(RunCMake_TEST_FAILED "${attr} attribute is not set" PARENT_SCOPE)
+    endif()
+  else()
+    if(NOT result_var)
+      set(RunCMake_TEST_FAILED "${attr} attribute is set" PARENT_SCOPE)
+    endif()
+  endif()
+endfunction()