ort-customops/tools/ios/build_xcframework.py

404 строки
15 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
# build an xcframework from individual per-platform/arch static frameworks
from __future__ import annotations
import argparse
import json
import shutil
import sys
from pathlib import Path
_repo_dir = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(_repo_dir / "tools"))
from utils import get_logger, run # noqa
_all_supported_platform_archs = {
"iphoneos": ["arm64"],
"iphonesimulator": ["x86_64", "arm64"],
"macosx": ["x86_64", "arm64"],
"maccatalyst": ["x86_64", "arm64"],
}
_default_supported_platform_archs = {
"iphoneos": ["arm64"],
"iphonesimulator": ["x86_64", "arm64"],
"macosx": ["x86_64", "arm64"],
}
_lipo = "lipo"
_xcrun = "xcrun"
_log = get_logger("build_xcframework")
def _get_opencv_toolchain_file(platform: str, opencv_dir: Path):
return (
opencv_dir
/ "platforms/ios/cmake/Toolchains"
/ ("Toolchain-iPhoneOS_Xcode.cmake" if platform == "iphoneos" else "Toolchain-iPhoneSimulator_Xcode.cmake")
)
def _rmtree_if_existing(dir: Path):
try:
shutil.rmtree(dir)
except FileNotFoundError:
pass
def _merge_framework_info_files(files: list[str], output_file: Path):
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_framework_for_platform_and_arch(
build_dir: Path,
platform: str,
arch: str,
config: str,
opencv_dir: Path,
ios_deployment_target: str,
macos_deployment_target: str,
other_build_args: list[str],
):
if platform == "maccatalyst":
apple_sysroot = "macosx"
else:
apple_sysroot = platform
build_cmd = [
sys.executable,
str(_repo_dir / "tools" / "build.py"),
f"--build_dir={build_dir}",
f"--config={config}",
"--update",
"--build",
"--parallel",
"--test",
"--build_apple_framework",
f"--apple_sysroot={apple_sysroot}",
f"--apple_arch={arch}",
]
cmake_defines = []
if platform != "macosx" and platform != "maccatalyst": # ios simulator or iphoneos platform
cmake_defines += [
# required by OpenCV CMake toolchain file
# https://github.com/opencv/opencv/blob/4223495e6cd67011f86b8ecd9be1fa105018f3b1/platforms/ios/cmake/Toolchains/common-ios-toolchain.cmake#L64-L66
f"IOS_ARCH={arch}",
# required by OpenCV CMake toolchain file
# https://github.com/opencv/opencv/blob/4223495e6cd67011f86b8ecd9be1fa105018f3b1/platforms/ios/cmake/Toolchains/common-ios-toolchain.cmake#L96-L101
f"IPHONEOS_DEPLOYMENT_TARGET={ios_deployment_target}",
]
# C++ test utils use std::filesystem which isn't available until iOS 13.
ios_major_version = int(ios_deployment_target.split(".")[0])
if ios_major_version < 13:
cmake_defines.append("OCOS_ENABLE_CTEST=OFF")
build_cmd += [
# iOS options
"--ios",
f"--ios_toolchain_file={_get_opencv_toolchain_file(platform, opencv_dir)}",
f"--apple_deploy_target={ios_deployment_target}",
]
elif platform == "macosx":
build_cmd += [
# macOS options
"--macos=MacOSX",
f"--apple_deploy_target={macos_deployment_target}",
]
else:
build_cmd += [
# mac catalyst options
"--macos=Catalyst",
f"--apple_deploy_target={ios_deployment_target}",
]
build_cmd += [f"--one_cmake_extra_define={cmake_define}" for cmake_define in cmake_defines]
build_cmd += other_build_args
run(*build_cmd)
def build_xcframework(
output_dir: Path,
platform_archs: dict[str, list[str]],
mode: str,
config: str,
opencv_dir: Path,
ios_deployment_target: str,
macos_deployment_target: str,
other_build_args: list[str],
):
output_dir = output_dir.resolve()
intermediate_build_dir = output_dir / "intermediates"
intermediate_build_dir.mkdir(parents=True, exist_ok=True)
assert len(platform_archs) > 0, "no platforms specified"
for platform, archs in platform_archs.items():
assert len(archs) > 0, f"no arch specified for platform {platform}"
def platform_arch_framework_build_dir(platform, arch):
return intermediate_build_dir / f"{platform}/{arch}"
build_platform_arch_frameworks = mode in ["build_platform_arch_frameworks_only", "build_xcframework"]
if build_platform_arch_frameworks:
for platform, archs in platform_archs.items():
for arch in archs:
build_framework_for_platform_and_arch(
platform_arch_framework_build_dir(platform, arch),
platform,
arch,
config,
opencv_dir,
ios_deployment_target,
macos_deployment_target,
other_build_args,
)
pack_xcframework = mode in ["pack_xcframework_only", "build_xcframework"]
if pack_xcframework:
# the public headers and framework_info.json should be the same across different archs per platform builds
# select one of them, merge into xcframework_info.json and copy to output directory
headers_dir = None
curr_framework_info_file = None
framework_info_files_to_merge = []
# create per-platform fat framework from platform/arch frameworks
platform_fat_framework_dirs = []
for platform, archs in platform_archs.items():
arch_framework_dirs = [
platform_arch_framework_build_dir(platform, arch)
/ config
/ "static_framework"
/ "onnxruntime_extensions.framework"
for arch in archs
]
if not build_platform_arch_frameworks:
# if they weren't just built, check that the expected platform/arch framework directories are present
for arch_framework_dir in arch_framework_dirs:
assert (
arch_framework_dir.is_dir()
), f"platform/arch framework directory not found: {arch_framework_dir}"
first_arch_framework_dir = arch_framework_dirs[0]
headers_dir = first_arch_framework_dir / "Headers"
curr_framework_info_file = first_arch_framework_dir.parents[1] / "framework_info.json"
framework_info_files_to_merge.append(curr_framework_info_file)
platform_fat_framework_dir = intermediate_build_dir / f"{platform}/onnxruntime_extensions.framework"
_rmtree_if_existing(platform_fat_framework_dir)
platform_fat_framework_dir.mkdir()
# copy over files from arch-specific framework to fat framework
# macos requires different framework structure:
# https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html
if platform == "macosx" or platform == "maccatalyst":
# Set up directory strcture
dest_headers_dir = platform_fat_framework_dir / "Versions/A/Headers"
dest_resources_dir = platform_fat_framework_dir / "Versions/A/Resources"
# Copy headers and Info.plist
shutil.copytree(first_arch_framework_dir / Path("Headers"), dest_headers_dir)
Path(dest_resources_dir).mkdir(parents=True, exist_ok=True)
shutil.copy(first_arch_framework_dir / Path("Info.plist"), dest_resources_dir / "Info.plist")
# combine arch-specific framework libraries
arch_libs = [str(framework_dir / "onnxruntime_extensions") for framework_dir in arch_framework_dirs]
run(
*(
[
_lipo,
"-create",
"-output",
str(platform_fat_framework_dir / "Versions/A/onnxruntime_extensions"),
]
+ arch_libs
)
)
# create Symbolic links
Path(platform_fat_framework_dir / "Versions/Current").symlink_to("A", target_is_directory=True)
Path(platform_fat_framework_dir / "Headers").symlink_to(
"Versions/Current/Headers", target_is_directory=True
)
Path(platform_fat_framework_dir / "Resources").symlink_to(
"Versions/Current/Resources", target_is_directory=True
)
Path(platform_fat_framework_dir / "onnxruntime_extensions").symlink_to(
"Versions/Current/onnxruntime_extensions"
)
else:
for framework_file_relative_path in [Path("Headers"), Path("Info.plist")]:
src = first_arch_framework_dir / framework_file_relative_path
dst = platform_fat_framework_dir / framework_file_relative_path
if src.is_dir():
shutil.copytree(src, dst)
else:
shutil.copy(src, dst)
# combine arch-specific framework libraries
arch_libs = [str(framework_dir / "onnxruntime_extensions") for framework_dir in arch_framework_dirs]
run(
*(
[_lipo, "-create", "-output", str(platform_fat_framework_dir / "onnxruntime_extensions")]
+ arch_libs
)
)
platform_fat_framework_dirs.append(platform_fat_framework_dir)
# create xcframework
xcframework_dir = output_dir / "onnxruntime_extensions.xcframework"
_rmtree_if_existing(xcframework_dir)
create_xcframework_cmd = [_xcrun, "xcodebuild", "-create-xcframework", "-output", str(xcframework_dir)]
for platform_fat_framework_dir in platform_fat_framework_dirs:
create_xcframework_cmd += ["-framework", str(platform_fat_framework_dir)]
run(*create_xcframework_cmd)
# copy public headers
output_headers_dir = output_dir / "Headers"
_rmtree_if_existing(output_headers_dir)
shutil.copytree(headers_dir, output_headers_dir, symlinks=True)
# merge framework_info.json per platform into xcframework_info.json in output_dir
_merge_framework_info_files(framework_info_files_to_merge, Path(output_dir, "xcframework_info.json"))
def parse_args():
parser = argparse.ArgumentParser(
description="Builds an iOS xcframework.",
)
parser.add_argument(
"--output_dir",
type=Path,
required=True,
help="Path to output directory.",
)
# This option is used in CI pipelines to accelerate the build process,
# We have multiple platform/archs to build for. We can build them in parallel.
# The parallel build works like this:
# 1. build the platform/arch frameworks in different jobs, in parallel
# 2. download the platform/arch framework files from the previous jobs and pack them into the xcframework
parser.add_argument(
"--mode",
default="build_xcframework",
choices=["build_xcframework", "build_platform_arch_frameworks_only", "pack_xcframework_only"],
help="Build mode. "
"'build_xcframework' builds the whole package. "
"'build_platform_arch_frameworks_only' builds the platform/arch frameworks only. "
"'pack_xcframework_only' packs the xcframework from existing lib files only. "
"Note: 'pack_xcframework_only' assumes previous invocation(s) with mode 'build_platform_arch_frameworks_only'.",
)
parser.add_argument(
"--platform_arch",
nargs=2,
action="append",
metavar=("PLATFORM", "ARCH"),
dest="platform_archs",
help="Specify a platform/arch pair to build. Repeat to specify multiple pairs. "
"If no pairs are specified, all supported pairs will be built.",
)
# platform/arch framework build-related options
parser.add_argument(
"--config",
choices=["Debug", "Release", "RelWithDebInfo", "MinSizeRel"],
default="Debug",
help="CMake build configuration.",
)
parser.add_argument(
"--ios_deployment_target",
default="12.0",
help="iOS deployment target.",
)
parser.add_argument(
"--macos_deployment_target",
default="11.0",
help="macOS deployment target.",
)
parser.add_argument(
"build_py_args",
nargs="*",
default=[],
help="Build arguments to pass through to build.py when building the platform/arch frameworks. "
"These should be placed after other arguments to this script following a trailing '--'. "
"For example: 'build_xcframework.py <build_xcframework.py options> -- <build.py options>'.",
)
args = parser.parse_args()
# convert from [[platform1, arch1], [platform1, arch2], ...] to {platform1: [arch1, arch2, ...], ...}
def platform_archs_from_args(platform_archs_arg: list[list[str]] | None) -> dict[str, list[str]]:
if not platform_archs_arg:
return _default_supported_platform_archs.copy()
platform_archs = {}
for platform, arch in platform_archs_arg:
assert (
platform in _all_supported_platform_archs.keys()
), f"Unsupported platform: '{platform}'. Valid values are {list(_all_supported_platform_archs.keys())}"
assert arch in _all_supported_platform_archs[platform], (
f"Unsupported arch for platform '{platform}': '{arch}'. "
f"Valid values are {_all_supported_platform_archs[platform]}"
)
archs = platform_archs.setdefault(platform, [])
if arch not in archs:
archs.append(arch)
return platform_archs
args.platform_archs = platform_archs_from_args(args.platform_archs)
return args
def main():
args = parse_args()
_log.info(f"Building xcframework for platform archs: {args.platform_archs}")
build_xcframework(
output_dir=args.output_dir,
platform_archs=args.platform_archs,
mode=args.mode,
config=args.config,
opencv_dir=_repo_dir / "cmake/externals/opencv",
ios_deployment_target=args.ios_deployment_target,
macos_deployment_target=args.macos_deployment_target,
other_build_args=args.build_py_args,
)
_log.info("xcframework build complete.")
if __name__ == "__main__":
main()