Skip to content

Move iOS package from framework to xcframework #8198

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ Pod::Spec.new do |spec|
spec.platform = :ios, '13.0'
# if you are going to use a file as the spec.source, add 'file:' before your file path
spec.source = { :http => '${ORT_BASE_FRAMEWORK_ARCHIVE}' }
spec.vendored_frameworks = 'onnxruntime.framework'
spec.source_files = 'onnxruntime.framework/Headers/*.h'
spec.vendored_frameworks = '${ORT_FRAMEWORK_NAME}'
end
83 changes: 59 additions & 24 deletions tools/ci_build/github/apple/build_ios_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
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'},
]
DEFAULT_BUILD_OSX_ARCHS = {
'iphoneos': ['arm64'],
'iphonesimulator': ['x86_64'],
}


def _parse_build_settings(args):
Expand All @@ -40,33 +40,22 @@ 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):
# 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)

Expand All @@ -75,17 +64,19 @@ def _build_package(args):
build_dir_current_arch, build_config, build_config + "-" + sysroot, 'onnxruntime.framework')
ort_libs.append(os.path.join(framework_dir, 'onnxruntime'))

# We actually only need to define the Info.plist and headers once since they are all the same
# We actually only need to define the Info.plist, podspec file and headers once since they are all the same
if not info_plist_path:
info_plist_path = os.path.join(build_dir_current_arch, build_config, 'Info.plist')
podspec_path = os.path.join(build_dir_current_arch, build_config, 'onnxruntime-mobile-c.podspec')
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')
pathlib.Path(framework_dir).mkdir(parents=True, exist_ok=True)

# copy the header files and Info.plist
# copy the header files, podspec file and Info.plist
shutil.copy(info_plist_path, framework_dir)
shutil.copy(podspec_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 @@ -97,14 +88,58 @@ 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 = []
podspec_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])
framework_dirs.append(framework_dir)
# podspec for each sysroot are the same, pick one of them
if not podspec_path:
podspec_path = os.path.join(os.path.dirname(framework_dir), 'onnxruntime-mobile-c.podspec')

# 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.copy(podspec_path, xcframework_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 Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
{
"build_osx_archs": [
{
"sysroot": "iphoneos",
"arch": "arm64"
},
{
"sysroot": "iphonesimulator",
"arch": "x86_64"
}
],
"build_osx_archs": {
"iphoneos": [
"arm64"
],
"iphonesimulator": [
"x86_64"
]
},
"build_params": [
"--ios",
"--parallel",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ Pod::Spec.new do |spec|
spec.name = "onnxruntime-mobile-c"
spec.version = "${ORT_VERSION}"
spec.authors = { "ONNX Runtime" => "[email protected]" }
spec.license = { :type => "MIT" }
spec.license = { :type => "MIT", :file => 'LICENSE' }
spec.homepage = "https://github.com/microsoft/onnxruntime"
spec.source = { :http => "_ORT_DOWNLOAD_URL_" }
spec.summary = "ONNX Runtime Mobile C/C++ Pod"
spec.platform = :ios, "${CMAKE_OSX_DEPLOYMENT_TARGET}"
spec.vendored_frameworks = "onnxruntime.framework"
spec.vendored_frameworks = "onnxruntime.xcframework"
spec.weak_framework = 'CoreML'
spec.source_files = 'onnxruntime.framework/Headers/*.h'

# arm64 iphonesimulator is not supported in this release
spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }
spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }

spec.description = <<-DESC
A preview pod for ONNX Runtime Mobile C/C++ library. Pods for Objective-C and Swift coming soon.
DESC
Expand Down
17 changes: 13 additions & 4 deletions tools/ci_build/github/apple/test_ios_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,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()
Copy link
Contributor

Choose a reason for hiding this comment

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

has_framework

why do we still want to support the old style here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now I don't think cmake can generate xcframework by itself, will still keep the framework for individual ABI, since we will also need it go generate xcframework

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 @@ -41,7 +49,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')
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 All @@ -56,6 +64,7 @@ def _test_ios_packages(args):

# replace the target strings
file_data = file_data.replace('${ORT_BASE_FRAMEWORK_ARCHIVE}', 'file:' + zip_file_path)
file_data = file_data.replace('${ORT_FRAMEWORK_NAME}', framework_name)
Copy link
Contributor

@edgchen1 edgchen1 Jun 30, 2021

Choose a reason for hiding this comment

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

file_data

could potentially specify the local pod directory in the Podfile
https://guides.cocoapods.org/using/the-podfile.html#using-the-files-from-a-folder-local-to-the-machine
then perhaps we could just have one podspec
just an idea - not necessarily for this PR

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 do in a future PR

with open(os.path.join(REPO_DIR, 'VERSION_NUMBER')) as version_file:
file_data = file_data.replace('${ORT_VERSION}', version_file.readline().strip())

Expand Down