Skip to content

Commit

Permalink
Support xcframework for mac catalyst builds. (microsoft#19534)
Browse files Browse the repository at this point in the history
### Description
<!-- Describe your changes. -->



### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->

MAUI on macOS uses mac-catalyst which requires a different native
binary.

---------

Co-authored-by: rachguo <[email protected]>
Co-authored-by: Scott McKay <[email protected]>
  • Loading branch information
3 people authored and Ted Themistokleous committed May 7, 2024
1 parent 37634b0 commit ac5a0e0
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 24 deletions.
9 changes: 9 additions & 0 deletions cmake/adjust_global_compile_flags.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Android")
string(APPEND CMAKE_ASM_FLAGS_RELEASE " -O3")
endif()

# Suggested by https://gitlab.kitware.com/cmake/cmake/-/issues/20132
# MacCatalyst is not well supported in CMake
# The error that can emerge without this flag can look like:
# "clang : error : overriding '-mmacosx-version-min=11.0' option with '-target x86_64-apple-ios14.0-macabi' [-Werror,-Woverriding-t-option]"
if (PLATFORM_NAME STREQUAL "macabi")
add_compile_options(-Wno-overriding-t-option)
add_link_options(-Wno-overriding-t-option)
endif()

# Enable space optimization for gcc/clang
# Cannot use "-ffunction-sections -fdata-sections" if we enable bitcode (iOS)
if (NOT MSVC AND NOT onnxruntime_ENABLE_BITCODE)
Expand Down
72 changes: 72 additions & 0 deletions cmake/maccatalyst_prepare_objects_for_prelink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os
import shutil
import sys


# Note: This script is mainly used for sanity checking/validating the files in the .a library equal to the .o files
# in the source dir to handle the case of source files having duplicate names under different subdirectories for
# each onnxruntime library. (Only applicable when doing a Mac Catalyst build.)
def main():
source_dir = sys.argv[1]
dest_dir = sys.argv[2]
files_from_static_lib = sys.argv[3]
files_from_source_dir = []
for subdir, _, files in os.walk(source_dir):
for file_name in files:
if file_name.endswith(".o"):
files_from_source_dir.append(file_name.strip())
dest_name_without_extension, _ = os.path.splitext(file_name)
counter = 0

dest_file = f"{dest_name_without_extension}.o"
while os.path.exists(os.path.join(dest_dir, dest_file)):
print("Duplicate file name from source: " + os.path.join(source_dir, subdir, file_name))
counter += 1
dest_file = f"{dest_name_without_extension}_{counter}.o"
print("Renamed file name in destination: " + os.path.join(dest_dir, dest_file))

destination_path = os.path.join(dest_dir, dest_file)
source_file = os.path.join(source_dir, subdir, file_name)
shutil.copy(source_file, destination_path)

# Sanity check to ensure the number of .o object from the original cmake source directory matches with the number
# of .o files extracted from each .a onnxruntime library
file_lists_from_static_lib = []
with open(files_from_static_lib) as file:
filenames = file.readlines()
for filename in filenames:
file_lists_from_static_lib.append(filename.strip())

sorted_list1 = sorted(file_lists_from_static_lib)
sorted_list2 = sorted(files_from_source_dir)

if len(sorted_list1) != len(sorted_list2):
print(
"Caught a mismatch in the number of .o object files from the original cmake source directory: ",
len(sorted_list1),
"the number of .o files extracted from the static onnxruntime lib: ",
len(sorted_list2),
"for: ",
os.path.basename(source_dir),
)

if sorted_list1 == sorted_list2:
print(
"Sanity check passed: object files from original source directory matches with files extracted "
"from static library for: ",
os.path.basename(source_dir),
)
else:
print(
"Error: Mismatch between object files from original source directory "
"and the .o files extracted from static library for: ",
os.path.basename(source_dir),
)


if __name__ == "__main__":
main()
36 changes: 29 additions & 7 deletions cmake/onnxruntime.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,13 @@ endif()

# Assemble the Apple static framework (iOS and macOS)
if(onnxruntime_BUILD_APPLE_FRAMEWORK)
set(STATIC_FRAMEWORK_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}-${CMAKE_OSX_SYSROOT})
# when building for mac catalyst, the CMAKE_OSX_SYSROOT is set to MacOSX as well, to avoid duplication,
# we specify as `-macabi` in the name of the output static apple framework directory.
if (PLATFORM_NAME STREQUAL "macabi")
set(STATIC_FRAMEWORK_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}-macabi)
else()
set(STATIC_FRAMEWORK_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}-${CMAKE_OSX_SYSROOT})
endif()

# Setup the various directories required. Remove any existing ones so we start with a clean directory.
set(STATIC_LIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/static_libraries)
Expand All @@ -299,18 +305,34 @@ if(onnxruntime_BUILD_APPLE_FRAMEWORK)
# to enforce symbol visibility. doing it this way limits the symbols included from the .a files to symbols used
# by the ORT .o files.

# If it's an onnxruntime library, extract .o files to a separate directory for each library to avoid any clashes
# with filenames (e.g. utils.o)
# If it's an onnxruntime library, extract .o files from the original cmake build path to a separate directory for
# each library to avoid any clashes with filenames (e.g. utils.o)
foreach(_LIB ${onnxruntime_INTERNAL_LIBRARIES} )
GET_TARGET_PROPERTY(_LIB_TYPE ${_LIB} TYPE)
if(_LIB_TYPE STREQUAL "STATIC_LIBRARY")
set(CUR_STATIC_LIB_OBJ_DIR ${STATIC_LIB_TEMP_DIR}/$<TARGET_LINKER_FILE_BASE_NAME:${_LIB}>)
add_custom_command(TARGET onnxruntime POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory ${CUR_STATIC_LIB_OBJ_DIR})

add_custom_command(TARGET onnxruntime POST_BUILD
COMMAND ar ARGS -x $<TARGET_FILE:${_LIB}>
WORKING_DIRECTORY ${CUR_STATIC_LIB_OBJ_DIR})
if (PLATFORM_NAME STREQUAL "macabi")
# There exists several duplicate names for source files under different subdirectories within
# each onnxruntime library. (e.g. onnxruntime/contrib_ops/cpu/element_wise_ops.o
# vs. onnxruntime/providers/core/cpu/math/element_wise_ops.o)
# In that case, using 'ar ARGS -x' to extract the .o files from .a lib would possibly cause duplicate naming files being overwritten
# and lead to missing undefined symbol error in the generated binary.
# So we use the below python script as a sanity check to do a recursive find of all .o files in ${CUR_TARGET_CMAKE_SOURCE_LIB_DIR}
# and verifies that matches the content of the .a, and then copy from the source dir.
# TODO: The copying action here isn't really necessary. For future fix, consider using the script extracts from the ar with the rename to potentially
# make both maccatalyst and other builds do the same thing.
set(CUR_TARGET_CMAKE_SOURCE_LIB_DIR ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_LIB}.dir)
add_custom_command(TARGET onnxruntime POST_BUILD
COMMAND ar -t $<TARGET_FILE:${_LIB}> | grep "\.o$" > ${_LIB}.object_file_list.txt
COMMAND ${CMAKE_COMMAND} -E env python3 ${CMAKE_CURRENT_SOURCE_DIR}/maccatalyst_prepare_objects_for_prelink.py ${CUR_TARGET_CMAKE_SOURCE_LIB_DIR} ${CUR_STATIC_LIB_OBJ_DIR} ${CUR_STATIC_LIB_OBJ_DIR}/${_LIB}.object_file_list.txt
WORKING_DIRECTORY ${CUR_STATIC_LIB_OBJ_DIR})
else()
add_custom_command(TARGET onnxruntime POST_BUILD
COMMAND ar ARGS -x $<TARGET_FILE:${_LIB}>
WORKING_DIRECTORY ${CUR_STATIC_LIB_OBJ_DIR})
endif()
endif()
endforeach()

Expand Down
6 changes: 6 additions & 0 deletions cmake/onnxruntime_mlas.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,12 @@ if (WIN32)
endif()
endif()

if (PLATFORM_NAME STREQUAL "macabi")
# Needed for maccatalyst C compilation
# i.e. the flags below add "--target=x86_64-apple-ios14.0-macabi -ffunction-sections -fdata-sections"
target_compile_options(onnxruntime_mlas PRIVATE ${CMAKE_C_FLAGS})
endif()

if (NOT onnxruntime_BUILD_SHARED_LIB)
install(TARGETS onnxruntime_mlas
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
229E595826586B4A006E41AE /* sigmoid.ort */ = {isa = PBXFileReference; lastKnownFileType = file; path = sigmoid.ort; sourceTree = "<group>"; };
22C1D8DE271A79AF002CEE67 /* ios_package_testUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ios_package_testUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
22C1D8E9271A79FD002CEE67 /* ios_package_uitest_cpp_api.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ios_package_uitest_cpp_api.mm; sourceTree = "<group>"; };
513C65792B85789400E4EDFD /* ios_package_test.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ios_package_test.entitlements; sourceTree = "<group>"; };
51C316B92B0881450033C70B /* macos_package_test.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = macos_package_test.app; sourceTree = BUILT_PRODUCTS_DIR; };
51C316BB2B0881450033C70B /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
51C316BC2B0881450033C70B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -117,6 +118,7 @@
229E591E265869BF006E41AE /* ios_package_test */ = {
isa = PBXGroup;
children = (
513C65792B85789400E4EDFD /* ios_package_test.entitlements */,
229E591F265869BF006E41AE /* AppDelegate.h */,
229E5920265869BF006E41AE /* AppDelegate.m */,
229E5928265869BF006E41AE /* Main.storyboard */,
Expand Down Expand Up @@ -521,18 +523,21 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CODE_SIGNING_REQUIRED = NO;
CODE_SIGNING_STYLE = Automatic;
CODE_SIGN_ENTITLEMENTS = ios_package_test/ios_package_test.entitlements;
INFOPLIST_FILE = ios_package_test/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "ai.onnxruntime.tests.ios-package-test";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
Expand All @@ -541,18 +546,21 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CODE_SIGNING_REQUIRED = NO;
CODE_SIGNING_STYLE = Automatic;
CODE_SIGN_ENTITLEMENTS = ios_package_test/ios_package_test.entitlements;
INFOPLIST_FILE = ios_package_test/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "ai.onnxruntime.tests.ios-package-test";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
Expand All @@ -563,7 +571,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -585,7 +593,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
42 changes: 38 additions & 4 deletions tools/ci_build/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ def convert_arg_line_to_args(self, arg_line):

parser.add_argument("--ios", action="store_true", help="build for ios")

parser.add_argument(
"--macos",
choices=["MacOSX", "Catalyst"],
help="Specify the target platform for macOS build. Only specify this argument when --build_apple_framework is present.",
)

parser.add_argument(
"--apple_sysroot", default="", help="Specify the location name of the macOS platform SDK to be used"
)
Expand All @@ -419,7 +425,7 @@ def convert_arg_line_to_args(self, arg_line):
action="store_const",
const="Xcode",
dest="cmake_generator",
help="Use Xcode as cmake generator, this is only supported on MacOS. Equivalent to '--cmake_generator Xcode'.",
help="Use Xcode as cmake generator, this is only supported on MacOS. (non Catalyst build). Equivalent to '--cmake_generator Xcode'.",
)
parser.add_argument(
"--osx_arch",
Expand Down Expand Up @@ -1323,8 +1329,12 @@ def generate_build_tree(
if args.use_snpe:
cmake_args += ["-Donnxruntime_USE_SNPE=ON"]

if args.build_apple_framework or args.ios:
if not args.cmake_generator == "Xcode":
if args.macos or args.ios:
# Note: Xcode CMake generator doesn't have a good support for Mac Catalyst yet.
if args.macos == "Catalyst" and args.cmake_generator == "Xcode":
raise BuildError("Xcode CMake generator ('--cmake_generator Xcode') doesn't support Mac Catalyst build.")

if (args.ios or args.macos == "MacOSX") and not args.cmake_generator == "Xcode":
raise BuildError(
"iOS/MacOS framework build requires use of the Xcode CMake generator ('--cmake_generator Xcode')."
)
Expand All @@ -1342,19 +1352,37 @@ def generate_build_tree(
"iOS/MacOS framework build on MacOS canceled due to missing arguments: "
+ ", ".join(val for val, cond in zip(arg_names, needed_args) if not cond)
)
# note: this value is mainly used in framework_info.json file to specify the build osx type
platform_name = "macabi" if args.macos == "Catalyst" else args.apple_sysroot
cmake_args += [
"-Donnxruntime_BUILD_SHARED_LIB=ON",
"-DCMAKE_OSX_SYSROOT=" + args.apple_sysroot,
"-DCMAKE_OSX_DEPLOYMENT_TARGET=" + args.apple_deploy_target,
# we do not need protoc binary for ios cross build
"-Dprotobuf_BUILD_PROTOC_BINARIES=OFF",
"-DPLATFORM_NAME=" + platform_name,
]
if args.ios:
cmake_args += [
"-DCMAKE_SYSTEM_NAME=iOS",
"-DCMAKE_TOOLCHAIN_FILE="
+ (args.ios_toolchain_file if args.ios_toolchain_file else "../cmake/onnxruntime_ios.toolchain.cmake"),
]
# for catalyst build, we need to manually specify cflags for target e.g. x86_64-apple-ios14.0-macabi, etc.
# https://forums.developer.apple.com/forums/thread/122571
if args.macos == "Catalyst":
macabi_target = f"{args.osx_arch}-apple-ios{args.apple_deploy_target}-macabi"
cmake_args += [
"-DCMAKE_CXX_COMPILER_TARGET=" + macabi_target,
"-DCMAKE_C_COMPILER_TARGET=" + macabi_target,
"-DCMAKE_CC_COMPILER_TARGET=" + macabi_target,
f"-DCMAKE_CXX_FLAGS=--target={macabi_target}",
f"-DCMAKE_CXX_FLAGS_RELEASE=-O3 -DNDEBUG --target={macabi_target}",
f"-DCMAKE_C_FLAGS=--target={macabi_target}",
f"-DCMAKE_C_FLAGS_RELEASE=-O3 -DNDEBUG --target={macabi_target}",
f"-DCMAKE_CC_FLAGS=--target={macabi_target}",
f"-DCMAKE_CC_FLAGS_RELEASE=-O3 -DNDEBUG --target={macabi_target}",
]

if args.build_wasm:
emsdk_dir = os.path.join(cmake_dir, "external", "emsdk")
Expand Down Expand Up @@ -2740,7 +2768,13 @@ def main():
cmake_extra_args += ["-G", args.cmake_generator]

if is_macOS():
if not args.ios and not args.android and args.osx_arch == "arm64" and platform.machine() == "x86_64":
if (
not args.ios
and args.macos != "Catalyst"
and not args.android
and args.osx_arch == "arm64"
and platform.machine() == "x86_64"
):
if args.test:
log.warning("Cannot test ARM64 build on X86_64. Will skip test running after build.")
args.test = False
Expand Down
4 changes: 3 additions & 1 deletion tools/ci_build/github/apple/build_apple_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ def _build_for_apple_sysroot(
# Build binary for each arch, one by one
for current_arch in archs:
build_dir_current_arch = os.path.join(intermediates_dir, sysroot + "_" + current_arch)
# Use MacOS SDK for Catalyst builds
apple_sysroot = "macosx" if sysroot == "macabi" else sysroot
build_command = [
*base_build_command,
"--apple_sysroot=" + sysroot,
"--apple_sysroot=" + apple_sysroot,
"--osx_arch=" + current_arch,
"--build_dir=" + build_dir_current_arch,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"--cmake_extra_defines=onnxruntime_BUILD_UNIT_TESTS=OFF"
],
"macosx": [
"--macos=MacOSX",
"--apple_deploy_target=11.0"
],
"iphoneos": [
Expand Down
Loading

0 comments on commit ac5a0e0

Please sign in to comment.