Xcode: Add support for combined install on iOS

This patch solves the problem of installing both: Device and Simulator
libraries on iOS. Before only one of them was installed.

If the IOS_INSTALL_COMBINED property is set on a target, a
special install hook will be activated which builds the corresponding
target and combines both at the install location.

The original patch was contributed by Ruslan Baratov, and polished by
Gregor Jasny.
This commit is contained in:
Ruslan Baratov 2015-10-08 03:09:34 +03:00 committed by Gregor Jasny
parent 34f5ef564a
commit 565d080a9a
16 changed files with 532 additions and 0 deletions

View File

@ -191,6 +191,7 @@ Properties on Targets
/prop_tgt/INTERFACE_SYSTEM_INCLUDE_DIRECTORIES
/prop_tgt/INTERPROCEDURAL_OPTIMIZATION_CONFIG
/prop_tgt/INTERPROCEDURAL_OPTIMIZATION
/prop_tgt/IOS_INSTALL_COMBINED
/prop_tgt/JOB_POOL_COMPILE
/prop_tgt/JOB_POOL_LINK
/prop_tgt/LABELS

View File

@ -257,6 +257,7 @@ Variables that Control the Build
/variable/CMAKE_INSTALL_NAME_DIR
/variable/CMAKE_INSTALL_RPATH
/variable/CMAKE_INSTALL_RPATH_USE_LINK_PATH
/variable/CMAKE_IOS_INSTALL_COMBINED
/variable/CMAKE_LANG_COMPILER_LAUNCHER
/variable/CMAKE_LANG_INCLUDE_WHAT_YOU_USE
/variable/CMAKE_LANG_VISIBILITY_PRESET

View File

@ -0,0 +1,11 @@
IOS_INSTALL_COMBINED
--------------------
Build a combined (device and simulator) target when installing.
When this property is set to set to false (which is the default) then it will
either be built with the device SDK or the simulator SDK depending on the SDK
set. But if this property is set to true then the target will at install time
also be built for the corresponding SDK and combined into one library.
This feature requires at least Xcode version 6.

View File

@ -0,0 +1,7 @@
ios-universal
-------------
* When building for embedded Apple platforms like iOS CMake learned to build and
install combined targets which contain both a device and a simulator build.
This behavior can be enabled by setting the :prop_tgt:`IOS_INSTALL_COMBINED`
target property.

View File

@ -0,0 +1,8 @@
CMAKE_IOS_INSTALL_COMBINED
--------------------------
Default value for :prop_tgt:`IOS_INSTALL_COMBINED` of targets.
This variable is used to initialize the :prop_tgt:`IOS_INSTALL_COMBINED`
property on all the targets. See that target property for additional
information.

View File

@ -0,0 +1,297 @@
#=============================================================================
# Copyright 2014-2015 Ruslan Baratov, Gregor Jasny
#
# Distributed under the OSI-approved BSD License (the "License");
# see accompanying file Copyright.txt for details.
#
# This software is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the License for more information.
#=============================================================================
# (To distribute this file outside of CMake, substitute the full
# License text for the above reference.)
# Function to print messages of this module
function(_ios_install_combined_message)
message("[iOS combined] " ${ARGN})
endfunction()
# Get build settings for the current target/config/SDK by running
# `xcodebuild -sdk ... -showBuildSettings` and parsing it's output
function(_ios_install_combined_get_build_setting sdk variable resultvar)
if("${sdk}" STREQUAL "")
message(FATAL_ERROR "`sdk` is empty")
endif()
if("${variable}" STREQUAL "")
message(FATAL_ERROR "`variable` is empty")
endif()
if("${resultvar}" STREQUAL "")
message(FATAL_ERROR "`resultvar` is empty")
endif()
set(
cmd
xcodebuild -showBuildSettings
-sdk "${sdk}"
-target "${CURRENT_TARGET}"
-config "${CURRENT_CONFIG}"
)
execute_process(
COMMAND ${cmd}
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
RESULT_VARIABLE result
OUTPUT_VARIABLE output
)
if(NOT result EQUAL 0)
message(FATAL_ERROR "Command failed (${result}): ${cmd}")
endif()
if(NOT output MATCHES " ${variable} = ([^\n]*)")
message(FATAL_ERROR "${variable} not found.")
endif()
set("${resultvar}" "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()
# Get architectures of given SDK (iphonesimulator/iphoneos)
function(_ios_install_combined_get_valid_archs sdk resultvar)
cmake_policy(SET CMP0007 NEW)
if("${resultvar}" STREQUAL "")
message(FATAL_ERROR "`resultvar` is empty")
endif()
_ios_install_combined_get_build_setting("${sdk}" "VALID_ARCHS" valid_archs)
separate_arguments(valid_archs)
list(REMOVE_ITEM valid_archs "") # remove empty elements
list(REMOVE_DUPLICATES valid_archs)
set("${resultvar}" "${valid_archs}" PARENT_SCOPE)
endfunction()
# Final target can contain more architectures that specified by SDK. This
# function will run 'lipo -info' and parse output. Result will be returned
# as a CMake list.
function(_ios_install_combined_get_real_archs filename resultvar)
set(cmd "${_lipo_path}" -info "${filename}")
execute_process(
COMMAND ${cmd}
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE output
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
)
if(NOT result EQUAL 0)
message(
FATAL_ERROR "Command failed (${result}): ${cmd}\n\nOutput:\n${output}"
)
endif()
if(NOT output MATCHES "(Architectures in the fat file: [^\n]+ are|Non-fat file: [^\n]+ is architecture): ([^\n]*)")
message(FATAL_ERROR "Could not detect architecture from: ${output}")
endif()
separate_arguments(CMAKE_MATCH_2)
set(${resultvar} ${CMAKE_MATCH_2} PARENT_SCOPE)
endfunction()
# Run build command for the given SDK
function(_ios_install_combined_build sdk)
if("${sdk}" STREQUAL "")
message(FATAL_ERROR "`sdk` is empty")
endif()
_ios_install_combined_message("Build `${CURRENT_TARGET}` for `${sdk}`")
execute_process(
COMMAND
"${CMAKE_COMMAND}"
--build
.
--target "${CURRENT_TARGET}"
--config ${CURRENT_CONFIG}
--
-sdk "${sdk}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
RESULT_VARIABLE result
)
if(NOT result EQUAL 0)
message(FATAL_ERROR "Build failed")
endif()
endfunction()
# Remove given architecture from file. This step needed only in rare cases
# when target was built in "unusual" way. Emit warning message.
function(_ios_install_combined_remove_arch lib arch)
_ios_install_combined_message(
"Warning! Unexpected architecture `${arch}` detected and will be removed "
"from file `${lib}`")
set(cmd "${_lipo_path}" -remove ${arch} -output ${lib} ${lib})
execute_process(
COMMAND ${cmd}
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE output
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
)
if(NOT result EQUAL 0)
message(
FATAL_ERROR "Command failed (${result}): ${cmd}\n\nOutput:\n${output}"
)
endif()
endfunction()
# Check that 'lib' contains only 'archs' architectures (remove others).
function(_ios_install_combined_keep_archs lib archs)
_ios_install_combined_get_real_archs("${lib}" real_archs)
set(archs_to_remove ${real_archs})
list(REMOVE_ITEM archs_to_remove ${archs})
foreach(x ${archs_to_remove})
_ios_install_combined_remove_arch("${lib}" "${x}")
endforeach()
endfunction()
function(_ios_install_combined_detect_sdks this_sdk_var corr_sdk_var)
cmake_policy(SET CMP0057 NEW)
set(this_sdk "$ENV{PLATFORM_NAME}")
if("${this_sdk}" STREQUAL "")
message(FATAL_ERROR "Environment variable PLATFORM_NAME is empty")
endif()
set(all_platforms "$ENV{SUPPORTED_PLATFORMS}")
if("${all_platforms}" STREQUAL "")
message(FATAL_ERROR "Environment variable SUPPORTED_PLATFORMS is empty")
endif()
separate_arguments(all_platforms)
if(NOT this_sdk IN_LIST all_platforms)
message(FATAL_ERROR "`${this_sdk}` not found in `${all_platforms}`")
endif()
list(REMOVE_ITEM all_platforms "" "${this_sdk}")
list(LENGTH all_platforms all_platforms_length)
if(NOT all_platforms_length EQUAL 1)
message(FATAL_ERROR "Expected one element: ${all_platforms}")
endif()
set(${this_sdk_var} "${this_sdk}" PARENT_SCOPE)
set(${corr_sdk_var} "${all_platforms}" PARENT_SCOPE)
endfunction()
# Create combined binary for the given target.
#
# Preconditions:
# * Target already installed at ${destination}
# for the ${PLATFORM_NAME} platform
#
# This function will:
# * Run build for the lacking platform, i.e. opposite to the ${PLATFORM_NAME}
# * Fuse both libraries by running lipo
function(ios_install_combined target destination)
if("${target}" STREQUAL "")
message(FATAL_ERROR "`target` is empty")
endif()
if("${destination}" STREQUAL "")
message(FATAL_ERROR "`destination` is empty")
endif()
if(NOT IS_ABSOLUTE "${destination}")
message(FATAL_ERROR "`destination` is not absolute: ${destination}")
endif()
if(IS_DIRECTORY "${destination}" OR IS_SYMLINK "${destination}")
message(FATAL_ERROR "`destination` is no regular file: ${destination}")
endif()
if("${CMAKE_BINARY_DIR}" STREQUAL "")
message(FATAL_ERROR "`CMAKE_BINARY_DIR` is empty")
endif()
if(NOT IS_DIRECTORY "${CMAKE_BINARY_DIR}")
message(FATAL_ERROR "Is not a directory: ${CMAKE_BINARY_DIR}")
endif()
if("${CMAKE_INSTALL_CONFIG_NAME}" STREQUAL "")
message(FATAL_ERROR "CMAKE_INSTALL_CONFIG_NAME is empty")
endif()
set(cmd xcrun -f lipo)
execute_process(
COMMAND ${cmd}
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE output
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
)
if(NOT result EQUAL 0)
message(
FATAL_ERROR "Command failed (${result}): ${cmd}\n\nOutput:\n${output}"
)
endif()
set(_lipo_path ${output})
set(CURRENT_CONFIG "${CMAKE_INSTALL_CONFIG_NAME}")
set(CURRENT_TARGET "${target}")
_ios_install_combined_message("Target: ${CURRENT_TARGET}")
_ios_install_combined_message("Config: ${CURRENT_CONFIG}")
_ios_install_combined_message("Destination: ${destination}")
# Get SDKs
_ios_install_combined_detect_sdks(this_sdk corr_sdk)
# Get architectures of the target
_ios_install_combined_get_valid_archs("${corr_sdk}" corr_valid_archs)
_ios_install_combined_get_valid_archs("${this_sdk}" this_valid_archs)
# Return if there are no valid architectures for the SDK.
# (note that library already installed)
if("${corr_valid_archs}" STREQUAL "")
_ios_install_combined_message(
"No architectures detected for `${corr_sdk}` (skip)"
)
return()
endif()
# Trigger build of corresponding target
_ios_install_combined_build("${corr_sdk}")
# Get location of the library in build directory
_ios_install_combined_get_build_setting(
"${corr_sdk}" "CONFIGURATION_BUILD_DIR" corr_build_dir)
_ios_install_combined_get_build_setting(
"${corr_sdk}" "EXECUTABLE_PATH" corr_executable_path)
set(corr "${corr_build_dir}/${corr_executable_path}")
_ios_install_combined_keep_archs("${corr}" "${corr_valid_archs}")
_ios_install_combined_keep_archs("${destination}" "${this_valid_archs}")
_ios_install_combined_message("Current: ${destination}")
_ios_install_combined_message("Corresponding: ${corr}")
set(cmd "${_lipo_path}" -create ${corr} ${destination} -output ${destination})
execute_process(
COMMAND ${cmd}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
RESULT_VARIABLE result
)
if(NOT result EQUAL 0)
message(FATAL_ERROR "Command failed: ${cmd}")
endif()
_ios_install_combined_message("Install done: ${destination}")
endfunction()

View File

@ -531,6 +531,7 @@ void cmInstallTargetGenerator::PostReplacementTweaks(std::ostream& os,
{
this->AddInstallNamePatchRule(os, indent, config, file);
this->AddChrpathPatchRule(os, indent, config, file);
this->AddUniversalInstallRule(os, indent, file);
this->AddRanlibRule(os, indent, file);
this->AddStripRule(os, indent, file);
}
@ -867,3 +868,46 @@ cmInstallTargetGenerator::AddRanlibRule(std::ostream& os,
os << indent << "execute_process(COMMAND \""
<< ranlib << "\" \"" << toDestDirPath << "\")\n";
}
//----------------------------------------------------------------------------
void
cmInstallTargetGenerator
::AddUniversalInstallRule(std::ostream& os,
Indent const& indent,
const std::string& toDestDirPath)
{
cmMakefile const* mf = this->Target->Target->GetMakefile();
if(!mf->PlatformIsAppleIos() || !mf->IsOn("XCODE"))
{
return;
}
const char* xcodeVersion = mf->GetDefinition("XCODE_VERSION");
if(!xcodeVersion || cmSystemTools::VersionCompareGreater("6", xcodeVersion))
{
return;
}
switch(this->Target->GetType())
{
case cmState::EXECUTABLE:
case cmState::STATIC_LIBRARY:
case cmState::SHARED_LIBRARY:
case cmState::MODULE_LIBRARY:
break;
default:
return;
}
if(!this->Target->Target->GetPropertyAsBool("IOS_INSTALL_COMBINED"))
{
return;
}
os << indent << "include(CMakeIOSInstallCombined)\n";
os << indent << "ios_install_combined("
<< "\"" << this->Target->Target->GetName() << "\" "
<< "\"" << toDestDirPath << "\")\n";
}

View File

@ -101,6 +101,8 @@ protected:
const std::string& toDestDirPath);
void AddRanlibRule(std::ostream& os, Indent const& indent,
const std::string& toDestDirPath);
void AddUniversalInstallRule(std::ostream& os, Indent const& indent,
const std::string& toDestDirPath);
std::string TargetName;
cmGeneratorTarget* Target;

View File

@ -132,6 +132,7 @@ void cmTarget::SetMakefile(cmMakefile* mf)
this->SetPropertyDefault("Fortran_MODULE_DIRECTORY", 0);
this->SetPropertyDefault("GNUtoMS", 0);
this->SetPropertyDefault("OSX_ARCHITECTURES", 0);
this->SetPropertyDefault("IOS_INSTALL_COMBINED", 0);
this->SetPropertyDefault("AUTOMOC", 0);
this->SetPropertyDefault("AUTOUIC", 0);
this->SetPropertyDefault("AUTORCC", 0);

View File

@ -94,3 +94,39 @@ if(NOT XCODE_VERSION VERSION_LESS 7)
run_cmake(XcodeTbdStub)
unset(RunCMake_TEST_OPTIONS)
endif()
if(NOT XCODE_VERSION VERSION_LESS 6)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/XcodeIOSInstallCombined-build)
set(RunCMake_TEST_NO_CLEAN 1)
set(RunCMake_TEST_OPTIONS
"-DCMAKE_INSTALL_PREFIX:PATH=${RunCMake_TEST_BINARY_DIR}/_install"
"-DCMAKE_IOS_INSTALL_COMBINED=YES")
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
run_cmake(XcodeIOSInstallCombined)
run_cmake_command(XcodeIOSInstallCombined-build ${CMAKE_COMMAND} --build .)
run_cmake_command(XcodeIOSInstallCombined-install ${CMAKE_COMMAND} --build . --target install)
unset(RunCMake_TEST_BINARY_DIR)
unset(RunCMake_TEST_NO_CLEAN)
unset(RunCMake_TEST_OPTIONS)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/XcodeIOSInstallCombinedPrune-build)
set(RunCMake_TEST_NO_CLEAN 1)
set(RunCMake_TEST_OPTIONS
"-DCMAKE_INSTALL_PREFIX:PATH=${RunCMake_TEST_BINARY_DIR}/_install"
"-DCMAKE_IOS_INSTALL_COMBINED=YES")
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
run_cmake(XcodeIOSInstallCombinedPrune)
run_cmake_command(XcodeIOSInstallCombinedPrune-build ${CMAKE_COMMAND} --build .)
run_cmake_command(XcodeIOSInstallCombinedPrune-install ${CMAKE_COMMAND} --build . --target install)
unset(RunCMake_TEST_BINARY_DIR)
unset(RunCMake_TEST_NO_CLEAN)
unset(RunCMake_TEST_OPTIONS)
endif()

View File

@ -0,0 +1,30 @@
function(verify_architectures file)
execute_process(
COMMAND xcrun otool -vf ${RunCMake_TEST_BINARY_DIR}/_install/${file}
OUTPUT_VARIABLE otool_out
ERROR_VARIABLE otool_err
RESULT_VARIABLE otool_result)
if(NOT otool_result EQUAL "0")
message(SEND_ERROR "Could not retrieve fat headers: ${otool_err}")
return()
endif()
string(REGEX MATCHALL "architecture [^ \n\t]+" architectures ${otool_out})
string(REPLACE "architecture " "" actual "${architectures}")
list(SORT actual)
set(expected arm64 armv7 i386 x86_64)
if(NOT actual STREQUAL expected)
message(SEND_ERROR
"The actual library contains the architectures:\n ${actual} \n"
"which do not match expected ones:\n ${expected} \n"
"otool output:\n${otool_out}")
endif()
endfunction()
verify_architectures(bin/foo_app.app/foo_app)
verify_architectures(lib/libfoo_static.a)
verify_architectures(lib/libfoo_shared.dylib)
verify_architectures(lib/foo_bundle.bundle/foo_bundle)
verify_architectures(lib/foo_framework.framework/foo_framework)

View File

@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.3)
project(IOSInstallCombined CXX)
set(CMAKE_OSX_SYSROOT iphoneos)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO")
set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf")
set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO")
set(CMAKE_OSX_ARCHITECTURES "armv7;arm64;i386;x86_64")
add_executable(foo_app MACOSX_BUNDLE main.cpp)
install(TARGETS foo_app BUNDLE DESTINATION bin)
add_library(foo_static STATIC foo.cpp)
install(TARGETS foo_static ARCHIVE DESTINATION lib)
add_library(foo_shared SHARED foo.cpp)
install(TARGETS foo_shared LIBRARY DESTINATION lib)
add_library(foo_bundle MODULE foo.cpp)
set_target_properties(foo_bundle PROPERTIES BUNDLE TRUE)
install(TARGETS foo_bundle LIBRARY DESTINATION lib)
add_library(foo_framework SHARED foo.cpp)
set_target_properties(foo_framework PROPERTIES FRAMEWORK TRUE)
install(TARGETS foo_framework FRAMEWORK DESTINATION lib)

View File

@ -0,0 +1,26 @@
function(verify_architectures file)
execute_process(
COMMAND xcrun otool -vf ${RunCMake_TEST_BINARY_DIR}/_install/${file}
OUTPUT_VARIABLE otool_out
ERROR_VARIABLE otool_err
RESULT_VARIABLE otool_result)
if(NOT otool_result EQUAL "0")
message(SEND_ERROR "Could not retrieve fat headers: ${otool_err}")
return()
endif()
string(REGEX MATCHALL "architecture [^ \n\t]+" architectures ${otool_out})
string(REPLACE "architecture " "" actual "${architectures}")
list(SORT actual)
set(expected armv7 x86_64)
if(NOT actual STREQUAL expected)
message(SEND_ERROR
"The actual library contains the architectures:\n ${actual} \n"
"which do not match expected ones:\n ${expected} \n"
"otool output:\n${otool_out}")
endif()
endfunction()
verify_architectures(lib/libfoo.dylib)

View File

@ -0,0 +1,2 @@
.*Unexpected architecture `i386` detected.*
.*Unexpected architecture `arm64` detected.*

View File

@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.3)
project(XcodeIOSInstallCombinedPrune CXX)
set(CMAKE_OSX_SYSROOT iphoneos)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO")
set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf")
add_library(foo SHARED foo.cpp)
install(TARGETS foo DESTINATION lib)
add_library(baz SHARED foo.cpp)
set_target_properties(
foo baz
PROPERTIES
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] armv7
XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphoneos*] armv7
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] x86_64
XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphonesimulator*] x86_64
)
add_library(boo SHARED foo.cpp)
set_target_properties(
boo
PROPERTIES
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] arm64
XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphoneos*] arm64
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] i386
XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphonesimulator*] i386
)
add_custom_command(
TARGET foo
POST_BUILD
COMMAND lipo -create $<TARGET_FILE:baz> $<TARGET_FILE:boo> -output $<TARGET_FILE:foo>
)

View File

@ -0,0 +1,3 @@
int main(int argc, const char * argv[]) {
return 0;
}