diff --git a/tools/ci_build/github/apple/build_ios_framework.py b/tools/ci_build/github/apple/build_ios_framework.py index cba3c5bd645e6..3d40a1c17ce46 100644 --- a/tools/ci_build/github/apple/build_ios_framework.py +++ b/tools/ci_build/github/apple/build_ios_framework.py @@ -15,11 +15,11 @@ REPO_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "..", "..")) BUILD_PY = os.path.join(REPO_DIR, "tools", "ci_build", "build.py") -# We by default will build below 2 archs -DEFAULT_BUILD_OSX_ARCHS = [ - {'sysroot': 'iphoneos', 'arch': 'arm64'}, - {'sysroot': 'iphonesimulator', 'arch': 'x86_64'}, -] +# We by default will build below 3 archs +DEFAULT_BUILD_OSX_ARCHS = { + 'iphoneos': ['arm64'], + 'iphonesimulator': ['arm64', 'x86_64'], +} def _parse_build_settings(args): @@ -40,23 +40,16 @@ def _parse_build_settings(args): return build_settings -def _build_package(args): - build_settings = _parse_build_settings(args) - build_dir = os.path.abspath(args.build_dir) - - # Temp dirs to hold building results - intermediates_dir = os.path.join(build_dir, 'intermediates') - build_config = args.config - base_build_command = [sys.executable, BUILD_PY, '--config=' + build_config] + build_settings['build_params'] - +# Build fat framework for all archs of a single sysroot +# For example, arm64 and x86_64 for iphonesimulator +def _build_for_ios_sysroot(build_config, intermediates_dir, base_build_command, + sysroot, archs, build_dynamic_framework): # paths of the onnxruntime libraries for different archs ort_libs = [] info_plist_path = '' # Build binary for each arch, one by one - for osx_arch in build_settings['build_osx_archs']: - sysroot = osx_arch['sysroot'] - current_arch = osx_arch['arch'] + for current_arch in archs: build_dir_current_arch = os.path.join(intermediates_dir, sysroot + "_" + current_arch) build_command = base_build_command + [ '--ios_sysroot=' + sysroot, @@ -64,16 +57,13 @@ def _build_package(args): '--build_dir=' + build_dir_current_arch ] - if args.include_ops_by_config is not None: - build_command += ['--include_ops_by_config=' + str(args.include_ops_by_config.resolve())] - # the actual build process for current arch subprocess.run(build_command, shell=False, check=True, cwd=REPO_DIR) # get the compiled lib path framework_dir = os.path.join( build_dir_current_arch, build_config, build_config + "-" + sysroot, - 'onnxruntime.framework' if args.build_dynamic_framework + 'onnxruntime.framework' if build_dynamic_framework else os.path.join('static_framework', 'onnxruntime.framework')) ort_libs.append(os.path.join(framework_dir, 'onnxruntime')) @@ -84,12 +74,15 @@ def _build_package(args): headers = glob.glob(os.path.join(framework_dir, 'Headers', '*.h')) # manually create the fat framework - framework_dir = os.path.join(build_dir, 'framework_out', 'onnxruntime.framework') + framework_dir = os.path.join(intermediates_dir, 'frameworks', sysroot, 'onnxruntime.framework') + # remove the existing framework if any + if os.path.exists(framework_dir): + shutil.rmtree(framework_dir) pathlib.Path(framework_dir).mkdir(parents=True, exist_ok=True) # copy the Info.plist, framework_info.json, and header files shutil.copy(info_plist_path, framework_dir) - shutil.copy(framework_info_path, build_dir) + shutil.copy(framework_info_path, os.path.dirname(framework_dir)) header_dir = os.path.join(framework_dir, 'Headers') pathlib.Path(header_dir).mkdir(parents=True, exist_ok=True) for _header in headers: @@ -101,14 +94,62 @@ def _build_package(args): lipo_command += ['-output', os.path.join(framework_dir, 'onnxruntime')] subprocess.run(lipo_command, shell=False, check=True) + return framework_dir + + +def _build_package(args): + build_settings = _parse_build_settings(args) + build_dir = os.path.abspath(args.build_dir) + + # Temp dirs to hold building results + intermediates_dir = os.path.join(build_dir, 'intermediates') + build_config = args.config + base_build_command = [sys.executable, BUILD_PY, '--config=' + build_config] + build_settings['build_params'] + if args.include_ops_by_config is not None: + base_build_command += ['--include_ops_by_config=' + str(args.include_ops_by_config.resolve())] + + # build framework for individual sysroot + framework_dirs = [] + framework_info_path = '' + public_headers_path = '' + for sysroot in build_settings['build_osx_archs']: + framework_dir = _build_for_ios_sysroot( + build_config, intermediates_dir, base_build_command, sysroot, + build_settings['build_osx_archs'][sysroot], args.build_dynamic_framework) + framework_dirs.append(framework_dir) + # podspec and headers for each sysroot are the same, pick one of them + if not framework_info_path: + framework_info_path = os.path.join(os.path.dirname(framework_dir), 'framework_info.json') + public_headers_path = os.path.join(os.path.dirname(framework_dir), 'onnxruntime.framework', 'Headers') + + # create the folder for xcframework and copy the LICENSE and podspec file + xcframework_dir = os.path.join(build_dir, 'framework_out') + pathlib.Path(xcframework_dir).mkdir(parents=True, exist_ok=True) + shutil.copy(os.path.join(REPO_DIR, 'LICENSE'), xcframework_dir) + shutil.copytree(public_headers_path, os.path.join(xcframework_dir, 'Headers'), dirs_exist_ok=True) + shutil.copy(framework_info_path, build_dir) + + # remove existing xcframework if any + xcframework_path = os.path.join(xcframework_dir, 'onnxruntime.xcframework') + if os.path.exists(xcframework_path): + shutil.rmtree(xcframework_path) + + # Assemble the final xcframework + build_xcframework_cmd = ['xcrun', 'xcodebuild', '-create-xcframework', + '-output', xcframework_path] + for framework_dir in framework_dirs: + build_xcframework_cmd.extend(['-framework', framework_dir]) + + subprocess.run(build_xcframework_cmd, shell=False, check=True, cwd=REPO_DIR) + def parse_args(): parser = argparse.ArgumentParser( os.path.basename(__file__), - description='''Create iOS framework and podspec for one or more osx_archs (fat framework) + description='''Create iOS framework and podspec for one or more osx_archs (xcframework) and building properties specified in the given build config file, see tools/ci_build/github/apple/default_mobile_ios_framework_build_settings.json for details. - The output of the final framework and podspec can be found under [build_dir]/framework_out. + The output of the final xcframework and podspec can be found under [build_dir]/framework_out. Please note, this building script will only work on macOS. ''' ) @@ -124,12 +165,12 @@ def parse_args(): choices=["Debug", "MinSizeRel", "Release", "RelWithDebInfo"], help="Configuration to build.") - parser.add_argument('build_settings_file', type=pathlib.Path, - help='Provide the file contains settings for building iOS framework') - parser.add_argument("--build_dynamic_framework", action='store_true', help="Build Dynamic Framework (default is build static framework).") + parser.add_argument('build_settings_file', type=pathlib.Path, + help='Provide the file contains settings for building iOS framework') + args = parser.parse_args() if not args.build_settings_file.resolve().is_file(): diff --git a/tools/ci_build/github/apple/c/assemble_c_pod_package.py b/tools/ci_build/github/apple/c/assemble_c_pod_package.py index 18dc8a19d23ce..261bc7eb6be4e 100644 --- a/tools/ci_build/github/apple/c/assemble_c_pod_package.py +++ b/tools/ci_build/github/apple/c/assemble_c_pod_package.py @@ -31,6 +31,8 @@ def parse_args(): parser.add_argument("--framework-info-file", type=pathlib.Path, required=True, help="Path to the framework_info.json file containing additional values for the podspec. " "This file should be generated by CMake in the build directory.") + parser.add_argument("--public-headers-dir", type=pathlib.Path, required=True, + help="Path to the public headers directory to include in the pod.") parser.add_argument("--framework-dir", type=pathlib.Path, required=True, help="Path to the onnxruntime.framework directory to include in the pod.") @@ -50,6 +52,8 @@ def main(): # copy the necessary files to the staging directory framework_dir = args.framework_dir.resolve() shutil.copytree(framework_dir, staging_dir / framework_dir.name, dirs_exist_ok=True) + public_headers_dir = args.public_headers_dir.resolve() + shutil.copytree(public_headers_dir, staging_dir / public_headers_dir.name, dirs_exist_ok=True) copy_repo_relative_to_dir(["LICENSE"], staging_dir) # generate the podspec file from the template diff --git a/tools/ci_build/github/apple/c/onnxruntime-mobile-c.podspec.template b/tools/ci_build/github/apple/c/onnxruntime-mobile-c.podspec.template index 710ad3734111d..eb6264793c9cd 100644 --- a/tools/ci_build/github/apple/c/onnxruntime-mobile-c.podspec.template +++ b/tools/ci_build/github/apple/c/onnxruntime-mobile-c.podspec.template @@ -7,20 +7,16 @@ Pod::Spec.new do |spec| spec.source = { :http => "file:///http_source_placeholder" } spec.summary = "ONNX Runtime Mobile C/C++ Pod" spec.platform = :ios, "@IOS_DEPLOYMENT_TARGET@" - spec.vendored_frameworks = "onnxruntime.framework" + spec.vendored_frameworks = "onnxruntime.xcframework" spec.static_framework = true spec.weak_framework = [ @WEAK_FRAMEWORK@ ] - spec.source_files = "onnxruntime.framework/Headers/*.h" + spec.source_files = "Headers/*.h" spec.preserve_paths = [ @LICENSE_FILE@ ] spec.description = <<-DESC A pod for the ONNX Runtime Mobile C/C++ library. DESC - spec.library = "c++" - spec.pod_target_xcconfig = { + spec.library = "c++" + spec.pod_target_xcconfig = { "OTHER_CPLUSPLUSFLAGS" => "-fvisibility=hidden -fvisibility-inlines-hidden", } - spec.user_target_xcconfig = { - # TODO workaround - support arm64 iphonesimulator later - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "arm64" - } end diff --git a/tools/ci_build/github/apple/default_mobile_ios_framework_build_settings.json b/tools/ci_build/github/apple/default_mobile_ios_framework_build_settings.json index da97470d642ee..e658b53f33210 100644 --- a/tools/ci_build/github/apple/default_mobile_ios_framework_build_settings.json +++ b/tools/ci_build/github/apple/default_mobile_ios_framework_build_settings.json @@ -1,14 +1,13 @@ { - "build_osx_archs": [ - { - "sysroot": "iphoneos", - "arch": "arm64" - }, - { - "sysroot": "iphonesimulator", - "arch": "x86_64" - } - ], + "build_osx_archs": { + "iphoneos": [ + "arm64" + ], + "iphonesimulator": [ + "arm64", + "x86_64" + ] + }, "build_params": [ "--ios", "--parallel", diff --git a/tools/ci_build/github/apple/objectivec/onnxruntime-mobile-objc.podspec.template b/tools/ci_build/github/apple/objectivec/onnxruntime-mobile-objc.podspec.template index e033f01636c79..cf3ca591743f0 100644 --- a/tools/ci_build/github/apple/objectivec/onnxruntime-mobile-objc.podspec.template +++ b/tools/ci_build/github/apple/objectivec/onnxruntime-mobile-objc.podspec.template @@ -30,7 +30,6 @@ Pod::Spec.new do |s| core.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => include_dirs.join(" "), "OTHER_CPLUSPLUSFLAGS" => "-fvisibility=hidden -fvisibility-inlines-hidden", - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "arm64", } core.public_header_files = [ diff --git a/tools/ci_build/github/apple/test_ios_packages.py b/tools/ci_build/github/apple/test_ios_packages.py index 03d1c60529111..78e5fe98bc7bf 100644 --- a/tools/ci_build/github/apple/test_ios_packages.py +++ b/tools/ci_build/github/apple/test_ios_packages.py @@ -31,9 +31,17 @@ def _test_ios_packages(args): if not c_framework_dir.is_dir(): raise FileNotFoundError('c_framework_dir {} is not a folder.'.format(c_framework_dir)) - framework_path = os.path.join(c_framework_dir, 'onnxruntime.framework') - if not pathlib.Path(framework_path).exists(): - raise FileNotFoundError('{} does not have onnxruntime.framework'.format(c_framework_dir)) + has_framework = pathlib.Path(os.path.join(c_framework_dir, 'onnxruntime.framework')).exists() + has_xcframework = pathlib.Path(os.path.join(c_framework_dir, 'onnxruntime.xcframework')).exists() + + if not has_framework and not has_xcframework: + raise FileNotFoundError('{} does not have onnxruntime.framework/xcframework'.format(c_framework_dir)) + + if has_framework and has_xcframework: + raise ValueError('Cannot proceed when both onnxruntime.framework ' + 'and onnxruntime.xcframework exist') + + framework_name = 'onnxruntime.framework' if has_framework else 'onnxruntime.xcframework' # create a temp folder import tempfile @@ -49,7 +57,7 @@ def _test_ios_packages(args): # shutil.make_archive require target file as full path without extension zip_base_filename = os.path.join(local_pods_dir, 'onnxruntime-mobile-c') zip_file_path = zip_base_filename + '.zip' - shutil.make_archive(zip_base_filename, 'zip', root_dir=c_framework_dir, base_dir='onnxruntime.framework') + shutil.make_archive(zip_base_filename, 'zip', root_dir=c_framework_dir, base_dir=framework_name) # copy the test project to the temp_dir test_proj_path = os.path.join(REPO_DIR, 'onnxruntime', 'test', 'platform', 'ios', 'ios_package_test') @@ -78,6 +86,13 @@ def _test_ios_packages(args): with open(podspec, 'r') as file: file_data = file.read() file_data = file_data.replace('file:///http_source_placeholder', 'file:' + zip_file_path) + + # We will only publish xcframework, however, assembly of the xcframework is a post process + # and it cannot be done by CMake for now. See, https://gitlab.kitware.com/cmake/cmake/-/issues/21752 + # For a single sysroot and arch built by build.py or cmake, we can only generate framework + # We still need a way to test it, replace the xcframework with framework in the podspec + if has_framework: + file_data = file_data.replace('onnxruntime.xcframework', 'onnxruntime.framework') with open(podspec, 'w') as file: file.write(file_data) diff --git a/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml b/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml index 1e4bf0170f55e..86d5bfa290293 100644 --- a/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml @@ -74,7 +74,8 @@ jobs: --staging-dir "$(Build.BinariesDirectory)/staging/onnxruntime-mobile-c" \ --pod-version ${ORT_POD_VERSION} \ --framework-info-file "$(Build.BinariesDirectory)/ios_framework/framework_info.json" \ - --framework-dir "$(Build.BinariesDirectory)/ios_framework/framework_out/onnxruntime.framework" + --framework-dir "$(Build.BinariesDirectory)/ios_framework/framework_out/onnxruntime.xcframework" \ + --public-headers-dir "$(Build.BinariesDirectory)/ios_framework/framework_out/Headers" displayName: "Assemble onnxruntime-mobile-c pod files" - script: |