Skip to content

Move iOS package from framework to xcframework #8805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 24, 2021
97 changes: 69 additions & 28 deletions tools/ci_build/github/apple/build_ios_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3

nit: if we don't mention the number it won't need to be updated again. could make this change next time though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove in a later PR

DEFAULT_BUILD_OSX_ARCHS = {
'iphoneos': ['arm64'],
'iphonesimulator': ['arm64', 'x86_64'],
}


def _parse_build_settings(args):
Expand All @@ -40,40 +40,30 @@ 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,
'--osx_arch=' + current_arch,
'--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'))

Expand All @@ -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:
Expand All @@ -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.
'''
)
Expand All @@ -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():
Expand Down
4 changes: 4 additions & 0 deletions tools/ci_build/github/apple/c/assemble_c_pod_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand All @@ -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 link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public_headers_dir

is public_headers_dir.name required to be "Headers" now? that seems to be the case from the template file

copy_repo_relative_to_dir(["LICENSE"], staging_dir)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LICENSE

do we need to copy the license file here if it's also copied to the xcframework in build_ios_framework.py?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we don't need to copy in multiple steps, we can copy everything from the framework_out directly, since it contains everything we need here,
Can change in a later PR


# generate the podspec file from the template
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
23 changes: 19 additions & 4 deletions tools/ci_build/github/apple/test_ios_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down