Skip to content

Commit

Permalink
Merge pull request opencv#18826 from Rightpoint:feature/colejd/build-…
Browse files Browse the repository at this point in the history
…catalyst-xcframework

Support XCFramework builds, Catalyst

* Early work on xcframework support

* Improve legibility

* Somehow this works

* Specify ABIs in a place where they won't get erased

If you pass in the C/CXX flags from the Python script, they won't be respected. By doing it in the actual toolchain, the options are respected and Catalyst successfully links.

* Clean up and push updates

* Actually use Catalyst ABI

Needed to specify EXE linker flags to get compiler tests to link to the Catalyst ABIs.

* Clean up

* Revert changes to common toolchain that don't matter

* Try some things

* Support Catalyst build in OSX scripts

* Remove unnecessary iOS reference to AssetsLibrary framework

* Getting closer

* Try some things, port to Python 3

* Some additional fixes

* Point Cmake Plist gen to osx directory for Catalyst targets

* Remove dynamic lib references for Catalyst, copy iOS instead of macos

* Add flag for building only specified archs, remove iOS catalyst refs

* Add build-xcframework.sh

* Update build-xcframework.sh

* Add presumptive Apple Silicon support

* Add arm64 iphonesimulator target

* Fix xcframework build

* Working on arm64 iOS simulator

* Support 2.7 (replace run with check_output)

* Correctly check output of uname_m against arch

* Clean up

* Use lipo for intermediate frameworks, add python script

Remove unneeded __init__.py

* Simplify python xcframework build script

* Add --only-64-bit flag

* Add --framework-name flag

* Document

* Commit to f-strings, improve console output

* Add i386 to iphonesimulator platform in xcframework generator

* Enable objc for non-Catalyst frameworks

* Fix xcframework builder for paths with spaces

* Use arch when specifying Catalyst build platform in build command

* Fix incorrect settings for framework_name argparse configuration

* Prefer underscores instead of hyphens in new flags

* Move Catalyst flags to where they'll actually get used

* Use --without=objc on Catalyst target for now

* Remove get_or_create_folder and simplify logic

* Remove unused import

* Tighten up help text

* Document

* Move common functions into cv_build_utils

* Improve documentation

* Remove old build script

* Add readme

* Check for required CMake and Xcode versions

* Clean up TODOs and re-enable `copy_samples()`

Remove TODO

Fixup

* Add missing print_function import

* Clarify CMake dependency documentation

* Revert python2 change in gen_objc

* Remove unnecessary builtins imports

* Remove trailing whitespace

* Avoid building Catalyst unless specified

This makes Catalyst support a non-breaking change, though defaults should be specified when a breaking change is possible.

* Prevent lipoing for the same archs on different platforms before build

* Rename build-xcframework.py to build_xcframework.py

* Check for duplicate archs more carefully

* Prevent sample copying error when directory already exists

This can happen when building multiple architectures for the same platform.

* Simplify code for checking for default archs

* Improve build_xcframework.py header text

* Correctly resolve Python script paths

* Parse only known args in ios/osx build_framework.py

* Pass through uncaptured args in build_xcframework to osx/ios build

* Fix typo

* Fix typo

* Fix unparameterized build path for intermediate frameworks

* Fix dyanmic info.plist path for catalyst

* Fix utf-8 Python 3 issue

* Add dynamic flag to osx script

* Rename platform to platforms, remove armv7s and i386

* Fix creation of dynamic framework on maccatalyst and macos

* Update platforms/apple/readme.md

* Add `macos_archs` flag and deprecate `archs` flag

* Allow specification of archs when generating xcframework from terminal

* Change xcframework platform argument names to match archs flag names

* Remove platforms as a concept and shadow archs flags from ios/osx .py

* Improve documentation

* Fix building of objc module on Catalyst, excluding Swift

* Clean up build folder logic a bit

* Fix framework_name flag

* Drop passthrough_args, use unknown_args instead

* minor: coding style changes

Co-authored-by: Chris Ballinger <[email protected]>
  • Loading branch information
colejd and chrisballinger authored Nov 24, 2020
1 parent 19d825a commit 85b0fb2
Show file tree
Hide file tree
Showing 14 changed files with 487 additions and 91 deletions.
6 changes: 5 additions & 1 deletion cmake/OpenCVGenInfoPlist.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ set(OPENCV_APPLE_BUNDLE_NAME "OpenCV")
set(OPENCV_APPLE_BUNDLE_ID "org.opencv")

if(IOS)
if (APPLE_FRAMEWORK AND DYNAMIC_PLIST)
if(MAC_CATALYST)
# Copy the iOS plist over to the OSX directory if building iOS library for Catalyst
configure_file("${OpenCV_SOURCE_DIR}/platforms/ios/Info.plist.in"
"${CMAKE_BINARY_DIR}/osx/Info.plist")
elseif(APPLE_FRAMEWORK AND DYNAMIC_PLIST)
configure_file("${OpenCV_SOURCE_DIR}/platforms/ios/Info.Dynamic.plist.in"
"${CMAKE_BINARY_DIR}/ios/Info.plist")
else()
Expand Down
8 changes: 7 additions & 1 deletion cmake/OpenCVUtils.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -1512,10 +1512,16 @@ function(ocv_add_library target)

set(CMAKE_SHARED_LIBRARY_RUNTIME_C_FLAG 1)

if(IOS AND NOT MAC_CATALYST)
set(OPENCV_APPLE_INFO_PLIST "${CMAKE_BINARY_DIR}/ios/Info.plist")
else()
set(OPENCV_APPLE_INFO_PLIST "${CMAKE_BINARY_DIR}/osx/Info.plist")
endif()

set_target_properties(${target} PROPERTIES
FRAMEWORK TRUE
MACOSX_FRAMEWORK_IDENTIFIER org.opencv
MACOSX_FRAMEWORK_INFO_PLIST ${CMAKE_BINARY_DIR}/ios/Info.plist
MACOSX_FRAMEWORK_INFO_PLIST ${OPENCV_APPLE_INFO_PLIST}
# "current version" in semantic format in Mach-O binary file
VERSION ${OPENCV_LIBVERSION}
# "compatibility version" in semantic format in Mach-O binary file
Expand Down
2 changes: 1 addition & 1 deletion modules/imgcodecs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ if(APPLE OR APPLE_FRAMEWORK)
endif()
if(IOS)
list(APPEND imgcodecs_srcs ${CMAKE_CURRENT_LIST_DIR}/src/ios_conversions.mm)
list(APPEND IMGCODECS_LIBRARIES "-framework UIKit" "-framework AssetsLibrary")
list(APPEND IMGCODECS_LIBRARIES "-framework UIKit")
endif()
if(APPLE AND (NOT IOS))
list(APPEND imgcodecs_srcs ${CMAKE_CURRENT_LIST_DIR}/src/macosx_conversions.mm)
Expand Down
10 changes: 7 additions & 3 deletions modules/objc/generator/templates/cmakelists.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ set (SUPPRESS_WARNINGS_FLAGS "-Wno-incomplete-umbrella")
set (CMAKE_CXX_FLAGS "$${CMAKE_CXX_FLAGS} $${OBJC_COMPILE_FLAGS} $${SUPPRESS_WARNINGS_FLAGS}")

# grab the files
file(GLOB_RECURSE objc_sources "objc/*\.h" "objc/*\.m" "objc/*\.mm" "objc/*\.swift" "objc/*\.modulemap")
if(SWIFT_DISABLED)
message(STATUS "Swift wrapper disabled")
file(GLOB_RECURSE objc_sources "objc/*\.h" "objc/*\.m" "objc/*\.mm" "objc/*\.modulemap")
else()
enable_language(Swift)
file(GLOB_RECURSE objc_sources "objc/*\.h" "objc/*\.m" "objc/*\.mm" "objc/*\.swift" "objc/*\.modulemap")
endif()
file(GLOB_RECURSE objc_headers "*\.h")

add_library($framework STATIC $${objc_sources})
Expand All @@ -29,8 +35,6 @@ endforeach()

install(TARGETS $framework LIBRARY DESTINATION lib)

enable_language(Swift)

# Additional target properties
if (CMAKE_XCODE_BUILD_SYSTEM GREATER_EQUAL 12)
set_target_properties($framework PROPERTIES
Expand Down
Empty file added platforms/apple/__init__.py
Empty file.
123 changes: 123 additions & 0 deletions platforms/apple/build_xcframework.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
This script builds OpenCV into an xcframework compatible with the platforms
of your choice. Just run it and grab a snack; you'll be waiting a while.
"""

import sys, os, argparse, pathlib, traceback
from cv_build_utils import execute, print_error, print_header, get_xcode_version, get_cmake_version

if __name__ == "__main__":

# Check for dependencies
assert sys.version_info >= (3, 6), f"Python 3.6 or later is required! Current version is {sys.version_info}"
# Need CMake 3.18.5/3.19 or later for a Silicon-related fix to building for the iOS Simulator.
# See https://gitlab.kitware.com/cmake/cmake/-/issues/21425 for context.
assert get_cmake_version() >= (3, 18, 5), f"CMake 3.18.5 or later is required. Current version is {get_cmake_version()}"
# Need Xcode 12.2 for Apple Silicon support
assert get_xcode_version() >= (12, 2), f"Xcode 12.2 command line tools or later are required! Current version is {get_xcode_version()}. \
Run xcode-select to switch if you have multiple Xcode installs."

# Parse arguments
description = """
This script builds OpenCV into an xcframework supporting the Apple platforms of your choice.
"""
epilog = """
Any arguments that are not recognized by this script are passed through to the ios/osx build_framework.py scripts.
"""
parser = argparse.ArgumentParser(description=description, epilog=epilog)
parser.add_argument('out', metavar='OUTDIR', help='The directory where the xcframework will be created')
parser.add_argument('--framework_name', default='opencv2', help='Name of OpenCV xcframework (default: opencv2, will change to OpenCV in future version)')
parser.add_argument('--iphoneos_archs', default=None, help='select iPhoneOS target ARCHS. Default is "armv7,arm64"')
parser.add_argument('--iphonesimulator_archs', default=None, help='select iPhoneSimulator target ARCHS. Default is "x86_64,arm64"')
parser.add_argument('--macos_archs', default=None, help='Select MacOS ARCHS. Default is "x86_64,arm64"')
parser.add_argument('--catalyst_archs', default=None, help='Select Catalyst ARCHS. Default is "x86_64,arm64"')
parser.add_argument('--build_only_specified_archs', default=False, action='store_true', help='if enabled, only directly specified archs are built and defaults are ignored')

args, unknown_args = parser.parse_known_args()
if unknown_args:
print(f"The following args are not recognized by this script and will be passed through to the ios/osx build_framework.py scripts: {unknown_args}")

# Parse architectures from args
iphoneos_archs = args.iphoneos_archs
if not iphoneos_archs and not args.build_only_specified_archs:
# Supply defaults
iphoneos_archs = "armv7,arm64"
print(f'Using iPhoneOS ARCHS={iphoneos_archs}')

iphonesimulator_archs = args.iphonesimulator_archs
if not iphonesimulator_archs and not args.build_only_specified_archs:
# Supply defaults
iphonesimulator_archs = "x86_64,arm64"
print(f'Using iPhoneSimulator ARCHS={iphonesimulator_archs}')

macos_archs = args.macos_archs
if not macos_archs and not args.build_only_specified_archs:
# Supply defaults
macos_archs = "x86_64,arm64"
print(f'Using MacOS ARCHS={macos_archs}')

catalyst_archs = args.macos_archs
if not catalyst_archs and not args.build_only_specified_archs:
# Supply defaults
catalyst_archs = "x86_64,arm64"
print(f'Using Catalyst ARCHS={catalyst_archs}')

# Build phase

try:
# Build .frameworks for each platform
osx_script_path = os.path.abspath(os.path.abspath(os.path.dirname(__file__))+'/../osx/build_framework.py')
ios_script_path = os.path.abspath(os.path.abspath(os.path.dirname(__file__))+'/../ios/build_framework.py')

build_folders = []

def get_or_create_build_folder(base_dir, platform):
build_folder = f"./{base_dir}/{platform}".replace(" ", "\\ ") # Escape spaces in output path
pathlib.Path(build_folder).mkdir(parents=True, exist_ok=True)
return build_folder

if iphoneos_archs:
build_folder = get_or_create_build_folder(args.out, "iphoneos")
build_folders.append(build_folder)
command = ["python3", ios_script_path, "--iphoneos_archs", iphoneos_archs, "--framework_name", args.framework_name, "--build_only_specified_archs", build_folder] + unknown_args
print_header("Building iPhoneOS frameworks")
print(command)
execute(command, cwd=os.getcwd())
if iphonesimulator_archs:
build_folder = get_or_create_build_folder(args.out, "iphonesimulator")
build_folders.append(build_folder)
command = ["python3", ios_script_path, "--iphonesimulator_archs", iphonesimulator_archs, "--framework_name", args.framework_name, "--build_only_specified_archs", build_folder] + unknown_args
print_header("Building iPhoneSimulator frameworks")
execute(command, cwd=os.getcwd())
if macos_archs:
build_folder = get_or_create_build_folder(args.out, "macos")
build_folders.append(build_folder)
command = ["python3", osx_script_path, "--macos_archs", macos_archs, "--framework_name", args.framework_name, "--build_only_specified_archs", build_folder] + unknown_args
print_header("Building MacOS frameworks")
execute(command, cwd=os.getcwd())
if catalyst_archs:
build_folder = get_or_create_build_folder(args.out, "catalyst")
build_folders.append(build_folder)
command = ["python3", osx_script_path, "--catalyst_archs", catalyst_archs, "--framework_name", args.framework_name, "--build_only_specified_archs", build_folder] + unknown_args
print_header("Building Catalyst frameworks")
execute(command, cwd=os.getcwd())

# Put all the built .frameworks together into a .xcframework
print_header("Building xcframework")
xcframework_build_command = [
"xcodebuild",
"-create-xcframework",
"-output",
f"{args.out}/{args.framework_name}.xcframework",
]
for folder in build_folders:
xcframework_build_command += ["-framework", f"{folder}/{args.framework_name}.framework"]
execute(xcframework_build_command, cwd=os.getcwd())

print("")
print_header(f"Finished building {args.out}/{args.framework_name}.xcframework")
except Exception as e:
print_error(e)
traceback.print_exc(file=sys.stderr)
sys.exit(1)
65 changes: 65 additions & 0 deletions platforms/apple/cv_build_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python
"""
Common utilities. These should be compatible with Python 2 and 3.
"""

from __future__ import print_function
import sys, re
from subprocess import check_call, check_output, CalledProcessError

def execute(cmd, cwd = None):
print("Executing: %s in %s" % (cmd, cwd), file=sys.stderr)
print('Executing: ' + ' '.join(cmd))
retcode = check_call(cmd, cwd = cwd)
if retcode != 0:
raise Exception("Child returned:", retcode)

def print_header(text):
print("="*60)
print(text)
print("="*60)

def print_error(text):
print("="*60, file=sys.stderr)
print("ERROR: %s" % text, file=sys.stderr)
print("="*60, file=sys.stderr)

def get_xcode_major():
ret = check_output(["xcodebuild", "-version"]).decode('utf-8')
m = re.match(r'Xcode\s+(\d+)\..*', ret, flags=re.IGNORECASE)
if m:
return int(m.group(1))
else:
raise Exception("Failed to parse Xcode version")

def get_xcode_version():
"""
Returns the major and minor version of the current Xcode
command line tools as a tuple of (major, minor)
"""
ret = check_output(["xcodebuild", "-version"]).decode('utf-8')
m = re.match(r'Xcode\s+(\d+)\.(\d+)', ret, flags=re.IGNORECASE)
if m:
return (int(m.group(1)), int(m.group(2)))
else:
raise Exception("Failed to parse Xcode version")

def get_xcode_setting(var, projectdir):
ret = check_output(["xcodebuild", "-showBuildSettings"], cwd = projectdir).decode('utf-8')
m = re.search("\s" + var + " = (.*)", ret)
if m:
return m.group(1)
else:
raise Exception("Failed to parse Xcode settings")

def get_cmake_version():
"""
Returns the major and minor version of the current CMake
command line tools as a tuple of (major, minor, revision)
"""
ret = check_output(["cmake", "--version"]).decode('utf-8')
m = re.match(r'cmake\sversion\s+(\d+)\.(\d+).(\d+)', ret, flags=re.IGNORECASE)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
else:
raise Exception("Failed to parse CMake version")
40 changes: 40 additions & 0 deletions platforms/apple/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Building for Apple Platforms

build_xcframework.py creates an xcframework supporting a variety of Apple platforms.

You'll need the following to run these steps:
- MacOS 10.15 or later
- Python 3.6 or later
- CMake 3.18.5/3.19.0 or later (make sure the `cmake` command is available on your PATH)
- Xcode 12.2 or later (and its command line tools)

You can then run build_xcframework.py, as below:
```
cd ~/<my_working_directory>
python opencv/platforms/apple/build_xcframework.py ./build_xcframework
```

Grab a coffee, because you'll be here for a while. By default this builds OpenCV for 8 architectures across 4 platforms:

- iOS (`--iphoneos_archs`): arm64, armv7
- iOS Simulator (`--iphonesimulator_archs`): x86_64, arm64
- macOS (`--macos_archs`): x86_64, arm64
- Mac Catalyst (`--catalyst_archs`): x86_64, arm64

If everything's fine, you will eventually get `opencv2.xcframework` in the output directory.

The script has some configuration options to exclude platforms and architectures you don't want to build for. Use the `--help` flag for more information.

## Examples

You may override the defaults by specifying a value for any of the `*_archs` flags. For example, if you want to build for arm64 on every platform, you can do this:

```
python build_xcframework.py somedir --iphoneos_archs arm64 --iphonesimulator_archs arm64 --macos_archs arm64 --catalyst_archs arm64
```

If you want to build only for certain platforms, you can supply the `--build_only_specified_archs` flag, which makes the script build only the archs you directly ask for. For example, to build only for Catalyst, you can do this:

```
python build_xcframework.py somedir --catalyst_archs x86_64,arm64 --build_only_specified_archs
```
Empty file added platforms/ios/__init__.py
Empty file.
Loading

0 comments on commit 85b0fb2

Please sign in to comment.