onnxruntime/tools/ci_build/github/apple/build_apple_framework.py
Rachel Guo 288b80d363
Add MacOS build to ORT C Pod (#18550)
### Description
<!-- Describe your changes. -->

As title.

1. Add macos build as an optionally enabled arch for pod and changes to
exsiting build_ios_framework/assemble_c_pod scripts.
2. Enable macos build arch in ios packaging pipeline (currently for
variants other than Mobile) and check the output artifacts are correct.
3. Write MacOS Test Target scheme in the test app and integrate into ios
packaging CI testing pipeline.
Currently the changes only apply to onnxruntime-c pod. as the original
request was from ORT SPM which consumes the onnxruntime-c pod only as
the binary target. TODO: could look into adding macos platform to objc
pod as well.

### 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. -->
Enable macos platform support in cocoapods. and also potentially produce
binary target for enabling macos platform in SPM as well.

Replace https://github.com/microsoft/onnxruntime/pull/18334

---------

Co-authored-by: rachguo <rachguo@rachguos-Mac-mini.local>
Co-authored-by: rachguo <rachguo@rachguos-Mini.attlocal.net>
Co-authored-by: Edward Chen <18449977+edgchen1@users.noreply.github.com>
2023-11-28 10:11:53 -08:00

244 lines
8.9 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import argparse
import glob
import json
import os
import pathlib
import shutil
import subprocess
import sys
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
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 3 archs
DEFAULT_BUILD_OSX_ARCHS = {
"iphoneos": ["arm64"],
"iphonesimulator": ["arm64", "x86_64"],
}
def _parse_build_settings(args):
with open(args.build_settings_file.resolve()) as f:
build_settings_data = json.load(f)
build_settings = {}
build_settings["build_osx_archs"] = build_settings_data.get("build_osx_archs", DEFAULT_BUILD_OSX_ARCHS)
if "build_params" in build_settings_data:
build_settings["build_params"] = build_settings_data["build_params"]
else:
raise ValueError("build_params is required in the build config file")
return build_settings
# Build fat framework for all archs of a single sysroot
# For example, arm64 and x86_64 for iphonesimulator
def _build_for_apple_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 current_arch in archs:
build_dir_current_arch = os.path.join(intermediates_dir, sysroot + "_" + current_arch)
build_command = [
*base_build_command,
"--apple_sysroot=" + sysroot,
"--osx_arch=" + current_arch,
"--build_dir=" + build_dir_current_arch,
]
# 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 build_dynamic_framework
else os.path.join("static_framework", "onnxruntime.framework"),
)
ort_libs.append(os.path.join(framework_dir, "onnxruntime"))
# We only need to copy Info.plist, framework_info.json, and headers once since they are the same
if not info_plist_path:
info_plist_path = os.path.join(build_dir_current_arch, build_config, "Info.plist")
framework_info_path = os.path.join(build_dir_current_arch, build_config, "framework_info.json")
headers = glob.glob(os.path.join(framework_dir, "Headers", "*.h"))
# manually create the fat 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, 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:
shutil.copy(_header, header_dir)
# use lipo to create a fat ort library
lipo_command = ["lipo", "-create"]
lipo_command += ort_libs
lipo_command += ["-output", os.path.join(framework_dir, "onnxruntime")]
subprocess.run(lipo_command, shell=False, check=True)
return framework_dir
def _merge_framework_info_files(files, output_file):
merged_data = {}
for file in files:
with open(file) as f:
data = json.load(f)
for platform, values in data.items():
assert platform not in merged_data, f"Duplicate platform value: {platform}"
merged_data[platform] = values
with open(output_file, "w") as f:
json.dump(merged_data, f, indent=2)
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
# build framework for individual sysroot
framework_dirs = []
framework_info_files_to_merge = []
public_headers_path = ""
for sysroot in build_settings["build_osx_archs"]:
base_build_command = (
[sys.executable, BUILD_PY]
+ build_settings["build_params"]["base"]
+ build_settings["build_params"][sysroot]
+ ["--config=" + build_config]
)
if args.include_ops_by_config is not None:
base_build_command += ["--include_ops_by_config=" + str(args.include_ops_by_config.resolve())]
if args.path_to_protoc_exe is not None:
base_build_command += ["--path_to_protoc_exe=" + str(args.path_to_protoc_exe.resolve())]
framework_dir = _build_for_apple_sysroot(
build_config,
intermediates_dir,
base_build_command,
sysroot,
build_settings["build_osx_archs"][sysroot],
args.build_dynamic_framework,
)
framework_dirs.append(framework_dir)
curr_framework_info_path = os.path.join(os.path.dirname(framework_dir), "framework_info.json")
framework_info_files_to_merge.append(curr_framework_info_path)
# headers for each sysroot are the same, pick one of them
if not public_headers_path:
public_headers_path = os.path.join(os.path.dirname(framework_dir), "onnxruntime.framework", "Headers")
# create the folder for xcframework and copy the LICENSE and framework_info.json 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)
_merge_framework_info_files(framework_info_files_to_merge, os.path.join(build_dir, "xcframework_info.json"))
# 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 (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 xcframework and podspec can be found under [build_dir]/framework_out.
Please note, this building script will only work on macOS.
""",
)
parser.add_argument(
"--build_dir",
type=pathlib.Path,
default=os.path.join(REPO_DIR, "build/apple_framework"),
help="Provide the root directory for build output",
)
parser.add_argument(
"--include_ops_by_config",
type=pathlib.Path,
help="Include ops from config file. See /docs/Reduced_Operator_Kernel_build.md for more information.",
)
parser.add_argument(
"--config",
type=str,
default="Release",
choices=["Debug", "MinSizeRel", "Release", "RelWithDebInfo"],
help="Configuration to build.",
)
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"
)
parser.add_argument("--path_to_protoc_exe", type=pathlib.Path, help="Path to protoc exe.")
args = parser.parse_args()
if not args.build_settings_file.resolve().is_file():
raise FileNotFoundError(f"Build config file {args.build_settings_file.resolve()} is not a file.")
if args.include_ops_by_config is not None:
include_ops_by_config_file = args.include_ops_by_config.resolve()
if not include_ops_by_config_file.is_file():
raise FileNotFoundError(f"Include ops config file {include_ops_by_config_file} is not a file.")
return args
def main():
args = parse_args()
_build_package(args)
if __name__ == "__main__":
main()