404 строки
15 KiB
Python
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()
|