diff --git a/cmake/OpenCVGenInfoPlist.cmake b/cmake/OpenCVGenInfoPlist.cmake index 90dd85479f..105087907f 100644 --- a/cmake/OpenCVGenInfoPlist.cmake +++ b/cmake/OpenCVGenInfoPlist.cmake @@ -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() diff --git a/cmake/OpenCVUtils.cmake b/cmake/OpenCVUtils.cmake index c8e7fdbd93..d07babdfbe 100644 --- a/cmake/OpenCVUtils.cmake +++ b/cmake/OpenCVUtils.cmake @@ -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 diff --git a/modules/imgcodecs/CMakeLists.txt b/modules/imgcodecs/CMakeLists.txt index 5a8faa9d05..8ae85e62c5 100644 --- a/modules/imgcodecs/CMakeLists.txt +++ b/modules/imgcodecs/CMakeLists.txt @@ -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) diff --git a/modules/objc/generator/templates/cmakelists.template b/modules/objc/generator/templates/cmakelists.template index 67cacbbfa4..10e9379694 100644 --- a/modules/objc/generator/templates/cmakelists.template +++ b/modules/objc/generator/templates/cmakelists.template @@ -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}) @@ -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 diff --git a/platforms/apple/__init__.py b/platforms/apple/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/platforms/apple/build_xcframework.py b/platforms/apple/build_xcframework.py new file mode 100755 index 0000000000..669d798ae4 --- /dev/null +++ b/platforms/apple/build_xcframework.py @@ -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) diff --git a/platforms/apple/cv_build_utils.py b/platforms/apple/cv_build_utils.py new file mode 100644 index 0000000000..d764b70fd1 --- /dev/null +++ b/platforms/apple/cv_build_utils.py @@ -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") diff --git a/platforms/apple/readme.md b/platforms/apple/readme.md new file mode 100644 index 0000000000..f12446c060 --- /dev/null +++ b/platforms/apple/readme.md @@ -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 ~/ +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 +``` diff --git a/platforms/ios/__init__.py b/platforms/ios/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/platforms/ios/build_framework.py b/platforms/ios/build_framework.py index 5965cd0a96..b7936ceea7 100755 --- a/platforms/ios/build_framework.py +++ b/platforms/ios/build_framework.py @@ -32,34 +32,14 @@ Adding --dynamic parameter will build {framework_name}.framework as App Store dy """ from __future__ import print_function, unicode_literals -import glob, re, os, os.path, shutil, string, sys, argparse, traceback, multiprocessing +import glob, os, os.path, shutil, string, sys, argparse, traceback, multiprocessing from subprocess import check_call, check_output, CalledProcessError from distutils.dir_util import copy_tree -IPHONEOS_DEPLOYMENT_TARGET='9.0' # default, can be changed via command line options or environment variable - -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 getXCodeMajor(): - 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") +sys.path.insert(0, os.path.abspath(os.path.abspath(os.path.dirname(__file__))+'/../apple')) +from cv_build_utils import execute, print_error, get_xcode_major, get_xcode_setting -def getXCodeSetting(var, projectdir): - ret = check_output(["xcodebuild", "-showBuildSettings"], cwd = projectdir) - m = re.search("\s" + var + " = (.*)", ret) - if m: - return m.group(1) - else: - raise Exception("Failed to parse Xcode settings") +IPHONEOS_DEPLOYMENT_TARGET='9.0' # default, can be changed via command line options or environment variable class Builder: def __init__(self, opencv, contrib, dynamic, bitcodedisabled, exclude, disable, enablenonfree, targets, debug, debug_info, framework_name, run_tests, build_docs): @@ -99,7 +79,7 @@ class Builder: main_working_dir = os.path.join(outdir, "build") dirs = [] - xcode_ver = getXCodeMajor() + xcode_ver = get_xcode_major() # build each architecture separately alltargets = [] @@ -119,6 +99,30 @@ class Builder: if xcode_ver >= 7 and target[1] == 'iPhoneOS' and self.bitcodedisabled == False: cmake_flags.append("-DCMAKE_C_FLAGS=-fembed-bitcode") cmake_flags.append("-DCMAKE_CXX_FLAGS=-fembed-bitcode") + if xcode_ver >= 7 and target[1] == 'Catalyst': + sdk_path = check_output(["xcodebuild", "-version", "-sdk", "macosx", "Path"]).decode('utf-8').rstrip() + c_flags = [ + "-target %s-apple-ios13.0-macabi" % target[0], # e.g. x86_64-apple-ios13.2-macabi # -mmacosx-version-min=10.15 + "-isysroot %s" % sdk_path, + "-iframework %s/System/iOSSupport/System/Library/Frameworks" % sdk_path, + "-isystem %s/System/iOSSupport/usr/include" % sdk_path, + ] + if self.bitcodedisabled == False: + c_flags.append("-fembed-bitcode") + cmake_flags.append("-DCMAKE_C_FLAGS=" + " ".join(c_flags)) + cmake_flags.append("-DCMAKE_CXX_FLAGS=" + " ".join(c_flags)) + cmake_flags.append("-DCMAKE_EXE_LINKER_FLAGS=" + " ".join(c_flags)) + + # CMake annot compile Swift for Catalyst https://gitlab.kitware.com/cmake/cmake/-/issues/21436 + # cmake_flags.append("-DCMAKE_Swift_FLAGS=" + " " + target_flag) + cmake_flags.append("-DSWIFT_DISABLED=1") + + cmake_flags.append("-DIOS=1") # Build the iOS codebase + cmake_flags.append("-DMAC_CATALYST=1") # Set a flag for Mac Catalyst, just in case we need it + cmake_flags.append("-DWITH_OPENCL=OFF") # Disable OpenCL; it isn't compatible with iOS + cmake_flags.append("-DCMAKE_OSX_SYSROOT=%s" % sdk_path) + cmake_flags.append("-DCMAKE_CXX_COMPILER_WORKS=TRUE") + cmake_flags.append("-DCMAKE_C_COMPILER_WORKS=TRUE") self.buildOne(target[0], target[1], main_build_dir, cmake_flags) if not self.dynamic: @@ -128,10 +132,10 @@ class Builder: self.makeFramework(outdir, dirs) if self.build_objc_wrapper: if self.run_tests: - check_call([sys.argv[0].replace("build_framework", "run_tests"), "--framework_dir=" + outdir, "--framework_name=" + self.framework_name, dirs[0] + "/modules/objc_bindings_generator/{}/test".format(self.getObjcTarget())]) + check_call([sys.argv[0].replace("build_framework", "run_tests"), "--framework_dir=" + outdir, "--framework_name=" + self.framework_name, dirs[0] + "/modules/objc_bindings_generator/{}/test".format(self.getObjcTarget(target[1]))]) else: print("To run tests call:") - print(sys.argv[0].replace("build_framework", "run_tests") + " --framework_dir=" + outdir + " --framework_name=" + self.framework_name + " " + dirs[0] + "/modules/objc_bindings_generator/{}/test".format(self.getObjcTarget())) + print(sys.argv[0].replace("build_framework", "run_tests") + " --framework_dir=" + outdir + " --framework_name=" + self.framework_name + " " + dirs[0] + "/modules/objc_bindings_generator/{}/test".format(self.getObjcTarget(target[1]))) if self.build_docs: check_call([sys.argv[0].replace("build_framework", "build_docs"), dirs[0] + "/modules/objc/framework_build"]) doc_path = os.path.join(dirs[0], "modules", "objc", "doc_build", "docs") @@ -147,9 +151,7 @@ class Builder: try: self._build(outdir) except Exception as e: - print("="*60, file=sys.stderr) - print("ERROR: %s" % e, file=sys.stderr) - print("="*60, file=sys.stderr) + print_error(e) traceback.print_exc(file=sys.stderr) sys.exit(1) @@ -170,17 +172,19 @@ class Builder: "-DOPENCV_INCLUDE_INSTALL_PATH=include", "-DOPENCV_3P_LIB_INSTALL_PATH=lib/3rdparty", "-DFRAMEWORK_NAME=%s" % self.framework_name, - ] + ([ - "-DBUILD_SHARED_LIBS=ON", - "-DCMAKE_MACOSX_BUNDLE=ON", - "-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=NO", - ] if self.dynamic and not self.build_objc_wrapper else []) + ([ - "-DDYNAMIC_PLIST=ON" - ] if self.dynamic else []) + ([ - "-DOPENCV_ENABLE_NONFREE=ON" - ] if self.enablenonfree else []) + ([ - "-DBUILD_WITH_DEBUG_INFO=ON" - ] if self.debug_info else []) + ] + if self.dynamic: + args += [ + "-DDYNAMIC_PLIST=ON" + ] + if self.enablenonfree: + args += [ + "-DOPENCV_ENABLE_NONFREE=ON" + ] + if self.debug_info: + args += [ + "-DBUILD_WITH_DEBUG_INFO=ON" + ] if len(self.exclude) > 0: args += ["-DBUILD_opencv_%s=OFF" % m for m in self.exclude] @@ -202,21 +206,18 @@ class Builder: buildcmd += [ "IPHONEOS_DEPLOYMENT_TARGET=" + os.environ['IPHONEOS_DEPLOYMENT_TARGET'], "ARCHS=%s" % arch, + "-sdk", target.lower(), + "-configuration", self.getConfiguration(), + "-parallelizeTargets", + "-jobs", str(multiprocessing.cpu_count()), ] - buildcmd += [ - "-sdk", target.lower(), - "-configuration", self.getConfiguration(), - "-parallelizeTargets", - "-jobs", str(multiprocessing.cpu_count()), - ] - return buildcmd def getInfoPlist(self, builddirs): return os.path.join(builddirs[0], "ios", "Info.plist") - def getObjcTarget(self): + def getObjcTarget(self, target): # Obj-C generation target return 'ios' @@ -226,9 +227,28 @@ class Builder: (["-DCMAKE_TOOLCHAIN_FILE=%s" % toolchain] if toolchain is not None else []) if target.lower().startswith("iphoneos"): cmakecmd.append("-DCPU_BASELINE=DETECT") + if target.lower().startswith("iphonesimulator"): + build_arch = check_output(["uname", "-m"]).decode('utf-8').rstrip() + if build_arch != arch: + print("build_arch (%s) != arch (%s)" % (build_arch, arch)) + cmakecmd.append("-DCMAKE_SYSTEM_PROCESSOR=" + arch) + cmakecmd.append("-DCMAKE_OSX_ARCHITECTURES=" + arch) + cmakecmd.append("-DCPU_BASELINE=DETECT") + cmakecmd.append("-DCMAKE_CROSSCOMPILING=ON") + cmakecmd.append("-DOPENCV_WORKAROUND_CMAKE_20989=ON") + if target.lower() == "catalyst": + build_arch = check_output(["uname", "-m"]).decode('utf-8').rstrip() + if build_arch != arch: + print("build_arch (%s) != arch (%s)" % (build_arch, arch)) + cmakecmd.append("-DCMAKE_SYSTEM_PROCESSOR=" + arch) + cmakecmd.append("-DCMAKE_OSX_ARCHITECTURES=" + arch) + cmakecmd.append("-DCPU_BASELINE=DETECT") + cmakecmd.append("-DCMAKE_CROSSCOMPILING=ON") + cmakecmd.append("-DOPENCV_WORKAROUND_CMAKE_20989=ON") if target.lower() == "macosx": - build_arch = check_output(["uname", "-m"]).rstrip() + build_arch = check_output(["uname", "-m"]).decode('utf-8').rstrip() if build_arch != arch: + print("build_arch (%s) != arch (%s)" % (build_arch, arch)) cmakecmd.append("-DCMAKE_SYSTEM_PROCESSOR=" + arch) cmakecmd.append("-DCMAKE_OSX_ARCHITECTURES=" + arch) cmakecmd.append("-DCPU_BASELINE=DETECT") @@ -249,7 +269,17 @@ class Builder: #cmakecmd.append(self.opencv) #cmakecmd.extend(cmakeargs) cmakecmd = self.makeCMakeCmd(arch, target, self.opencv, cmakeargs) + print("") + print("=================================") + print("CMake") + print("=================================") + print("") execute(cmakecmd, cwd = builddir) + print("") + print("=================================") + print("Xcodebuild") + print("=================================") + print("") # Clean and build clean_dir = os.path.join(builddir, "install") @@ -259,7 +289,9 @@ class Builder: execute(buildcmd + ["-target", "ALL_BUILD", "build"], cwd = builddir) execute(["cmake", "-DBUILD_TYPE=%s" % self.getConfiguration(), "-P", "cmake_install.cmake"], cwd = builddir) if self.build_objc_wrapper: - cmakecmd = self.makeCMakeCmd(arch, target, builddir + "/modules/objc_bindings_generator/{}/gen".format(self.getObjcTarget()), cmakeargs) + cmakecmd = self.makeCMakeCmd(arch, target, builddir + "/modules/objc_bindings_generator/{}/gen".format(self.getObjcTarget(target)), cmakeargs) + # cmakecmd.append("-DCMAKE_Swift_FLAGS=" + "-target x86_64-apple-ios13.0-macabi") + # cmakecmd.append("-DCMAKE_EXE_LINKER_FLAGS=" + "-target x86_64-apple-ios13.0-macabi") cmakecmd.append("-DBUILD_ROOT=%s" % builddir) cmakecmd.append("-DCMAKE_INSTALL_NAME_TOOL=install_name_tool") cmakecmd.append("--no-warn-unused-cli") @@ -280,24 +312,51 @@ class Builder: def makeDynamicLib(self, builddir): target = builddir[(builddir.rfind("build-") + 6):] target_platform = target[(target.rfind("-") + 1):] - is_device = target_platform == "iphoneos" - res = os.path.join(builddir, "install", "lib", self.framework_name + ".framework", self.framework_name) + is_device = target_platform == "iphoneos" or target_platform == "catalyst" + framework_dir = os.path.join(builddir, "install", "lib", self.framework_name + ".framework") + if not os.path.exists(framework_dir): + os.makedirs(framework_dir) + res = os.path.join(framework_dir, self.framework_name) libs = glob.glob(os.path.join(builddir, "install", "lib", "*.a")) - module = [os.path.join(builddir, "lib", self.getConfiguration(), self.framework_name + ".framework", self.framework_name)] + if self.build_objc_wrapper: + module = [os.path.join(builddir, "lib", self.getConfiguration(), self.framework_name + ".framework", self.framework_name)] + else: + module = [] libs3 = glob.glob(os.path.join(builddir, "install", "lib", "3rdparty", "*.a")) - link_target = target[:target.find("-")] + "-apple-ios" + os.environ['IPHONEOS_DEPLOYMENT_TARGET'] + ("-simulator" if target.endswith("simulator") else "") + if os.environ.get('IPHONEOS_DEPLOYMENT_TARGET'): + link_target = target[:target.find("-")] + "-apple-ios" + os.environ['IPHONEOS_DEPLOYMENT_TARGET'] + ("-simulator" if target.endswith("simulator") else "") + else: + if target_platform == "catalyst": + link_target = "%s-apple-ios13.0-macabi" % target[:target.find("-")] + else: + link_target = "%s-apple-darwin" % target[:target.find("-")] bitcode_flags = ["-fembed-bitcode", "-Xlinker", "-bitcode_verify"] if is_device and not self.bitcodedisabled else [] - toolchain_dir = getXCodeSetting("TOOLCHAIN_DIR", builddir) + toolchain_dir = get_xcode_setting("TOOLCHAIN_DIR", builddir) + sdk_dir = get_xcode_setting("SDK_DIR", builddir) + framework_options = [] swift_link_dirs = ["-L" + toolchain_dir + "/usr/lib/swift/" + target_platform, "-L/usr/lib/swift"] - sdk_dir = getXCodeSetting("SDK_DIR", builddir) + if target_platform == "catalyst": + swift_link_dirs = ["-L" + toolchain_dir + "/usr/lib/swift/" + "maccatalyst", "-L/usr/lib/swift"] + framework_options = [ + "-iframework", "%s/System/iOSSupport/System/Library/Frameworks" % sdk_dir, + "-framework", "AVFoundation", "-framework", "UIKit", "-framework", "CoreGraphics", + "-framework", "CoreImage", "-framework", "CoreMedia", "-framework", "QuartzCore", + ] + elif target_platform == "macosx": + framework_options = [ + "-framework", "AVFoundation", "-framework", "AppKit", "-framework", "CoreGraphics", + "-framework", "CoreImage", "-framework", "CoreMedia", "-framework", "QuartzCore", + "-framework", "Accelerate", "-framework", "OpenCL", + ] execute([ "clang++", "-Xlinker", "-rpath", "-Xlinker", "/usr/lib/swift", "-target", link_target, - "-isysroot", sdk_dir, + "-isysroot", sdk_dir,] + + framework_options + [ "-install_name", "@rpath/" + self.framework_name + ".framework/" + self.framework_name, "-dynamiclib", "-dead_strip", "-fobjc-link-runtime", "-all_load", "-o", res @@ -402,6 +461,8 @@ class iOSBuilder(Builder): def copy_samples(self, outdir): print('Copying samples to: ' + outdir) samples_dir = os.path.join(outdir, "samples") + if os.path.exists(samples_dir): + shutil.rmtree(samples_dir) shutil.copytree(os.path.join(self.opencv, "samples", "swift", "ios"), samples_dir) if self.framework_name != "OpenCV": for dirname, dirs, files in os.walk(samples_dir): @@ -430,8 +491,9 @@ if __name__ == "__main__": parser.add_argument('--dynamic', default=False, action='store_true', help='build dynamic framework (default is "False" - builds static framework)') parser.add_argument('--disable-bitcode', default=False, dest='bitcodedisabled', action='store_true', help='disable bitcode (enabled by default)') parser.add_argument('--iphoneos_deployment_target', default=os.environ.get('IPHONEOS_DEPLOYMENT_TARGET', IPHONEOS_DEPLOYMENT_TARGET), help='specify IPHONEOS_DEPLOYMENT_TARGET') - parser.add_argument('--iphoneos_archs', default='armv7,armv7s,arm64', help='select iPhoneOS target ARCHS') - parser.add_argument('--iphonesimulator_archs', default='i386,x86_64', help='select iPhoneSimulator target ARCHS') + 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') + parser.add_argument('--iphoneos_archs', default=None, help='select iPhoneOS target ARCHS. Default is "armv7,armv7s,arm64"') + parser.add_argument('--iphonesimulator_archs', default=None, help='select iPhoneSimulator target ARCHS. Default is "i386,x86_64"') parser.add_argument('--enable_nonfree', default=False, dest='enablenonfree', action='store_true', help='enable non-free modules (disabled by default)') parser.add_argument('--debug', default=False, dest='debug', action='store_true', help='Build "Debug" binaries (disabled by default)') parser.add_argument('--debug_info', default=False, dest='debug_info', action='store_true', help='Build with debug information (useful for Release mode: BUILD_WITH_DEBUG_INFO=ON)') @@ -440,26 +502,58 @@ if __name__ == "__main__": parser.add_argument('--run_tests', default=False, dest='run_tests', action='store_true', help='Run tests') parser.add_argument('--build_docs', default=False, dest='build_docs', action='store_true', help='Build docs') - args = parser.parse_args() + args, unknown_args = parser.parse_known_args() + if unknown_args: + print("The following args are not recognized and will not be used: %s" % unknown_args) os.environ['IPHONEOS_DEPLOYMENT_TARGET'] = args.iphoneos_deployment_target print('Using IPHONEOS_DEPLOYMENT_TARGET=' + os.environ['IPHONEOS_DEPLOYMENT_TARGET']) - iphoneos_archs = args.iphoneos_archs.split(',') + + iphoneos_archs = None + if args.iphoneos_archs: + iphoneos_archs = args.iphoneos_archs.split(',') + elif not args.build_only_specified_archs: + # Supply defaults + iphoneos_archs = ["armv7", "armv7s", "arm64"] print('Using iPhoneOS ARCHS=' + str(iphoneos_archs)) - iphonesimulator_archs = args.iphonesimulator_archs.split(',') + + iphonesimulator_archs = None + if args.iphonesimulator_archs: + iphonesimulator_archs = args.iphonesimulator_archs.split(',') + elif not args.build_only_specified_archs: + # Supply defaults + iphonesimulator_archs = ["i386", "x86_64"] print('Using iPhoneSimulator ARCHS=' + str(iphonesimulator_archs)) + + # Prevent the build from happening if the same architecture is specified for multiple platforms. + # When `lipo` is run to stitch the frameworks together into a fat framework, it'll fail, so it's + # better to stop here while we're ahead. + if iphoneos_archs and iphonesimulator_archs: + duplicate_archs = set(iphoneos_archs).intersection(iphonesimulator_archs) + if duplicate_archs: + print_error("Cannot have the same architecture for multiple platforms in a fat framework! Consider using build_xcframework.py in the apple platform folder instead. Duplicate archs are %s" % duplicate_archs) + exit(1) + if args.legacy_build: args.framework_name = "opencv2" if not "objc" in args.without: args.without.append("objc") - b = iOSBuilder(args.opencv, args.contrib, args.dynamic, args.bitcodedisabled, args.without, args.disable, args.enablenonfree, - [ - (iphoneos_archs, "iPhoneOS"), - ] if os.environ.get('BUILD_PRECOMMIT', None) else - [ - (iphoneos_archs, "iPhoneOS"), - (iphonesimulator_archs, "iPhoneSimulator"), - ], args.debug, args.debug_info, args.framework_name, args.run_tests, args.build_docs) + targets = [] + if os.environ.get('BUILD_PRECOMMIT', None): + if not iphoneos_archs: + print_error("--iphoneos_archs must have at least one value") + sys.exit(1) + targets.append((iphoneos_archs, "iPhoneOS")) + else: + if not iphoneos_archs and not iphonesimulator_archs: + print_error("--iphoneos_archs and --iphonesimulator_archs are undefined; nothing will be built.") + sys.exit(1) + if iphoneos_archs: + targets.append((iphoneos_archs, "iPhoneOS")) + if iphonesimulator_archs: + targets.append((iphonesimulator_archs, "iPhoneSimulator")) + + b = iOSBuilder(args.opencv, args.contrib, args.dynamic, args.bitcodedisabled, args.without, args.disable, args.enablenonfree, targets, args.debug, args.debug_info, args.framework_name, args.run_tests, args.build_docs) b.build(args.out) diff --git a/platforms/ios/cmake/Toolchains/Toolchain-Catalyst_Xcode.cmake b/platforms/ios/cmake/Toolchains/Toolchain-Catalyst_Xcode.cmake new file mode 100644 index 0000000000..a22c10a7af --- /dev/null +++ b/platforms/ios/cmake/Toolchains/Toolchain-Catalyst_Xcode.cmake @@ -0,0 +1,4 @@ +set(MAC_CATALYST TRUE) +message(STATUS "Setting up Catalyst toolchain for IOS_ARCH='${IOS_ARCH}'") +include(${CMAKE_CURRENT_LIST_DIR}/common-ios-toolchain.cmake) +message(STATUS "Catalyst toolchain loaded") diff --git a/platforms/ios/cmake/Toolchains/common-ios-toolchain.cmake b/platforms/ios/cmake/Toolchains/common-ios-toolchain.cmake index 13aea357f1..4cbe4f1729 100644 --- a/platforms/ios/cmake/Toolchains/common-ios-toolchain.cmake +++ b/platforms/ios/cmake/Toolchains/common-ios-toolchain.cmake @@ -79,8 +79,11 @@ endif() if(NOT DEFINED CMAKE_OSX_SYSROOT) if(IPHONEOS) set(CMAKE_OSX_SYSROOT "iphoneos") - else() + elseif(IPHONESIMULATOR) set(CMAKE_OSX_SYSROOT "iphonesimulator") + elseif(MAC_CATALYST) + # Use MacOS SDK for Catalyst builds + set(CMAKE_OSX_SYSROOT "macosx") endif() endif() set(CMAKE_MACOSX_BUNDLE YES) @@ -90,7 +93,7 @@ if(APPLE_FRAMEWORK AND NOT BUILD_SHARED_LIBS) set(CMAKE_OSX_ARCHITECTURES "${IOS_ARCH}" CACHE INTERNAL "Build architecture for iOS" FORCE) endif() -if(NOT DEFINED IPHONEOS_DEPLOYMENT_TARGET) +if(NOT DEFINED IPHONEOS_DEPLOYMENT_TARGET AND NOT MAC_CATALYST) if(NOT DEFINED ENV{IPHONEOS_DEPLOYMENT_TARGET}) message(FATAL_ERROR "IPHONEOS_DEPLOYMENT_TARGET is not specified") endif() diff --git a/platforms/osx/__init__.py b/platforms/osx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/platforms/osx/build_framework.py b/platforms/osx/build_framework.py index de13e665fa..95c8268454 100755 --- a/platforms/osx/build_framework.py +++ b/platforms/osx/build_framework.py @@ -9,14 +9,19 @@ import os, os.path, sys, argparse, traceback, multiprocessing # import common code sys.path.insert(0, os.path.abspath(os.path.abspath(os.path.dirname(__file__))+'/../ios')) from build_framework import Builder +sys.path.insert(0, os.path.abspath(os.path.abspath(os.path.dirname(__file__))+'/../apple')) +from cv_build_utils import print_error MACOSX_DEPLOYMENT_TARGET='10.12' # default, can be changed via command line options or environment variable class OSXBuilder(Builder): - def getObjcTarget(self): + def getObjcTarget(self, target): # Obj-C generation target - return 'osx' + if target == "Catalyst": + return 'ios' + else: + return 'osx' def getToolchain(self, arch, target): return None @@ -26,11 +31,21 @@ class OSXBuilder(Builder): "xcodebuild", "MACOSX_DEPLOYMENT_TARGET=" + os.environ['MACOSX_DEPLOYMENT_TARGET'], "ARCHS=%s" % arch, - "-sdk", target.lower(), + "-sdk", "macosx" if target == "Catalyst" else target.lower(), "-configuration", "Debug" if self.debug else "Release", "-parallelizeTargets", "-jobs", str(multiprocessing.cpu_count()) ] + + if target == "Catalyst": + buildcmd.append("-destination 'platform=macOS,arch=%s,variant=Mac Catalyst'" % arch) + buildcmd.append("-UseModernBuildSystem=YES") + buildcmd.append("SKIP_INSTALL=NO") + buildcmd.append("BUILD_LIBRARY_FOR_DISTRIBUTION=YES") + buildcmd.append("TARGETED_DEVICE_FAMILY=\"1,2\"") + buildcmd.append("SDKROOT=iphoneos") + buildcmd.append("SUPPORTS_MAC_CATALYST=YES") + return buildcmd def getInfoPlist(self, builddirs): @@ -45,30 +60,68 @@ if __name__ == "__main__": parser.add_argument('--contrib', metavar='DIR', default=None, help='folder with opencv_contrib repository (default is "None" - build only main framework)') parser.add_argument('--without', metavar='MODULE', default=[], action='append', help='OpenCV modules to exclude from the framework') parser.add_argument('--disable', metavar='FEATURE', default=[], action='append', help='OpenCV features to disable (add WITH_*=OFF)') + parser.add_argument('--dynamic', default=False, action='store_true', help='build dynamic framework (default is "False" - builds static framework)') parser.add_argument('--enable_nonfree', default=False, dest='enablenonfree', action='store_true', help='enable non-free modules (disabled by default)') parser.add_argument('--macosx_deployment_target', default=os.environ.get('MACOSX_DEPLOYMENT_TARGET', MACOSX_DEPLOYMENT_TARGET), help='specify MACOSX_DEPLOYMENT_TARGET') - parser.add_argument('--archs', default='x86_64', help='Select target ARCHS (set to "x86_64,arm64" to build Universal Binary for Big Sur and later)') + 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') + parser.add_argument('--archs', default=None, help='(Deprecated! Prefer --macos_archs instead.) Select target ARCHS (set to "x86_64,arm64" to build Universal Binary for Big Sur and later). Default is "x86_64".') + parser.add_argument('--macos_archs', default=None, help='Select target ARCHS (set to "x86_64,arm64" to build Universal Binary for Big Sur and later). Default is "x86_64"') + parser.add_argument('--catalyst_archs', default=None, help='Select target ARCHS (set to "x86_64,arm64" to build Universal Binary for Big Sur and later). Default is None') parser.add_argument('--debug', action='store_true', help='Build "Debug" binaries (CMAKE_BUILD_TYPE=Debug)') parser.add_argument('--debug_info', action='store_true', help='Build with debug information (useful for Release mode: BUILD_WITH_DEBUG_INFO=ON)') - parser.add_argument('--framework_name', default='opencv2', dest='framework_name', action='store_true', help='Name of OpenCV framework (default: opencv2, will change to OpenCV in future version)') + parser.add_argument('--framework_name', default='opencv2', dest='framework_name', help='Name of OpenCV framework (default: opencv2, will change to OpenCV in future version)') parser.add_argument('--legacy_build', default=False, dest='legacy_build', action='store_true', help='Build legacy framework (default: False, equivalent to "--framework_name=opencv2 --without=objc")') parser.add_argument('--run_tests', default=False, dest='run_tests', action='store_true', help='Run tests') parser.add_argument('--build_docs', default=False, dest='build_docs', action='store_true', help='Build docs') - args = parser.parse_args() + args, unknown_args = parser.parse_known_args() + if unknown_args: + print("The following args are not recognized and will not be used: %s" % unknown_args) os.environ['MACOSX_DEPLOYMENT_TARGET'] = args.macosx_deployment_target print('Using MACOSX_DEPLOYMENT_TARGET=' + os.environ['MACOSX_DEPLOYMENT_TARGET']) - archs = args.archs.split(',') - print('Using ARCHS=' + str(archs)) + + macos_archs = None + if args.archs: + # The archs flag is replaced by macos_archs. If the user specifies archs, + # treat it as if the user specified the macos_archs flag instead. + args.macos_archs = args.archs + print("--archs is deprecated! Prefer --macos_archs instead.") + if args.macos_archs: + macos_archs = args.macos_archs.split(',') + elif not args.build_only_specified_archs: + # Supply defaults + macos_archs = ["x86_64"] + print('Using MacOS ARCHS=' + str(macos_archs)) + + catalyst_archs = None + if args.catalyst_archs: + catalyst_archs = args.catalyst_archs.split(',') + # TODO: To avoid breaking existing CI, catalyst_archs has no defaults. When we can make a breaking change, this should specify a default arch. + print('Using Catalyst ARCHS=' + str(catalyst_archs)) + + # Prevent the build from happening if the same architecture is specified for multiple platforms. + # When `lipo` is run to stitch the frameworks together into a fat framework, it'll fail, so it's + # better to stop here while we're ahead. + if macos_archs and catalyst_archs: + duplicate_archs = set(macos_archs).intersection(catalyst_archs) + if duplicate_archs: + print_error("Cannot have the same architecture for multiple platforms in a fat framework! Consider using build_xcframework.py in the apple platform folder instead. Duplicate archs are %s" % duplicate_archs) + exit(1) if args.legacy_build: args.framework_name = "opencv2" if not "objc" in args.without: args.without.append("objc") - b = OSXBuilder(args.opencv, args.contrib, False, False, args.without, args.disable, args.enablenonfree, - [ - (archs, "MacOSX") - ], args.debug, args.debug_info, args.framework_name, args.run_tests, args.build_docs) + targets = [] + if not macos_archs and not catalyst_archs: + print_error("--macos_archs and --catalyst_archs are undefined; nothing will be built.") + sys.exit(1) + if macos_archs: + targets.append((macos_archs, "MacOSX")) + if catalyst_archs: + targets.append((catalyst_archs, "Catalyst")), + + b = OSXBuilder(args.opencv, args.contrib, args.dynamic, True, args.without, args.disable, args.enablenonfree, targets, args.debug, args.debug_info, args.framework_name, args.run_tests, args.build_docs) b.build(args.out)