From 7c35c007c74ddf194faf6f960ba30b389ac97a7e Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:08:14 -0800 Subject: [PATCH 01/24] update build_apple_framework.py to support split builds of frameworks --- .../github/apple/build_apple_framework.py | 451 ++++++++++++------ 1 file changed, 307 insertions(+), 144 deletions(-) diff --git a/tools/ci_build/github/apple/build_apple_framework.py b/tools/ci_build/github/apple/build_apple_framework.py index 5a3b242c2a389..b4790a051450a 100644 --- a/tools/ci_build/github/apple/build_apple_framework.py +++ b/tools/ci_build/github/apple/build_apple_framework.py @@ -2,18 +2,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from __future__ import annotations + import argparse -import glob import json import os import pathlib +import re import shutil import subprocess import sys +from dataclasses import dataclass -SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) -REPO_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "..", "..")) -BUILD_PY = os.path.join(REPO_DIR, "tools", "ci_build", "build.py") +SCRIPT_DIR = pathlib.Path(__file__).parent.resolve() +REPO_DIR = SCRIPT_DIR.parents[3] +BUILD_PY = REPO_DIR / "tools" / "ci_build" / "build.py" # We by default will build below 3 archs DEFAULT_BUILD_OSX_ARCHS = { @@ -22,124 +25,208 @@ } -def _parse_build_settings(args): - with open(args.build_settings_file.resolve()) as f: +def _parse_build_settings(build_settings_file: pathlib.Path) -> dict: + with open(build_settings_file) as f: build_settings_data = json.load(f) build_settings = {} - build_settings["build_osx_archs"] = build_settings_data.get("build_osx_archs", DEFAULT_BUILD_OSX_ARCHS) - - if "build_params" in build_settings_data: - build_settings["build_params"] = build_settings_data["build_params"] - else: - raise ValueError("build_params is required in the build config file") + build_settings["build_params"] = build_settings_data.get("build_params", {}) return build_settings -# Build fat framework for all archs of a single sysroot -# For example, arm64 and x86_64 for iphonesimulator -def _build_for_apple_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 = "" +def _get_sysroot_arch_pairs_from_build_settings(build_settings) -> list[tuple[str, str]]: + pair_set: set[tuple[str, str]] = set() + for sysroot, archs in build_settings["build_osx_archs"].items(): + for arch in archs: + pair_set.add((sysroot, arch)) + return sorted(list(pair_set)) + - # Build binary for each arch, one by one - for current_arch in archs: - build_dir_current_arch = os.path.join(intermediates_dir, sysroot + "_" + current_arch) +def _filter_sysroot_arch_pairs( + all_sysroot_arch_pairs: list[tuple[str, str]], + args, +) -> list[tuple[str, str]]: + if args.only_build_single_sysroot_arch_framework is not None: + specified_sysroot_arch_pair = ( + args.only_build_single_sysroot_arch_framework[0], + args.only_build_single_sysroot_arch_framework[1], + ) + if specified_sysroot_arch_pair not in all_sysroot_arch_pairs: + raise ValueError( + f"Sysroot/arch pair is not present in build settings file. " + f"Specified: {specified_sysroot_arch_pair}, available: {all_sysroot_arch_pairs}" + ) + + return [specified_sysroot_arch_pair] + + if args.only_build_sysroot_arch_framework_by_partition is not None: + + def parse(partition_id: str) -> tuple[int, int]: + match = re.fullmatch(r"^(\d+)/(\d+)$", partition_id) + if not match: + raise ValueError( + f"Invalid partition ID: {partition_id}. Expected format is /." + ) + numerator, denominator = int(match.group(1)), int(match.group(2)) + if numerator == 0 or numerator > denominator: + raise ValueError( + f"Invalid partition ID: {partition_id}. " + "Numerator must be non-zero and not greater than denominator." + ) + return numerator, denominator + + one_based_partition_idx, num_partitions = parse(args.only_build_sysroot_arch_framework_by_partition) + partition_idx = one_based_partition_idx - 1 + + return all_sysroot_arch_pairs[partition_idx::num_partitions] + + return all_sysroot_arch_pairs.copy() + + +# info related to a framework for a single sysroot and arch (e.g., iphoneos/arm64) +@dataclass +class SysrootArchFrameworkInfo: + framework_dir: pathlib.Path + framework_info_file: pathlib.Path + info_plist_file: pathlib.Path + + +# find or build the sysroot/arch framework +# if `base_build_command` is not None, the framework will be built +def _find_or_build_sysroot_arch_framework( + build_config: str, + intermediates_dir: pathlib.Path, + base_build_command: list[str] | None, + sysroot: str, + arch: str, + build_dynamic_framework: bool, +) -> SysrootArchFrameworkInfo: + do_build = base_build_command is not None + + build_dir_current_arch = intermediates_dir / f"{sysroot}_{arch}" + + if do_build: # Use MacOS SDK for Catalyst builds apple_sysroot = "macosx" if sysroot == "macabi" else sysroot build_command = [ *base_build_command, - "--apple_sysroot=" + apple_sysroot, - "--osx_arch=" + current_arch, - "--build_dir=" + build_dir_current_arch, + f"--apple_sysroot={apple_sysroot}", + f"--osx_arch={arch}", + f"--build_dir={build_dir_current_arch}", ] - # 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 build_dynamic_framework - else os.path.join("static_framework", "onnxruntime.framework") - ), - ) - ort_libs.append(os.path.join(framework_dir, "onnxruntime")) + # build framework for specified sysroot/arch + subprocess.run(build_command, shell=False, check=True) + + # get the compiled lib path + framework_dir = pathlib.Path.joinpath( + build_dir_current_arch, + build_config, + build_config + "-" + sysroot, + ( + "onnxruntime.framework" + if build_dynamic_framework + else pathlib.Path("static_framework/onnxruntime.framework") + ), + ) + + info_plist_file = build_dir_current_arch / build_config / "Info.plist" + framework_info_file = build_dir_current_arch / build_config / "framework_info.json" + + if not do_build: + for expected_path in [framework_dir, info_plist_file, framework_info_file]: + if not expected_path.exists(): + raise FileNotFoundError(f"Expected framework path does not exist: {expected_path}") + + return SysrootArchFrameworkInfo( + framework_dir=framework_dir, + info_plist_file=info_plist_file, + framework_info_file=framework_info_file, + ) + - # We only need to copy Info.plist, framework_info.json, and headers once since they are the same - if not info_plist_path: - info_plist_path = os.path.join(build_dir_current_arch, build_config, "Info.plist") - framework_info_path = os.path.join(build_dir_current_arch, build_config, "framework_info.json") - headers = glob.glob(os.path.join(framework_dir, "Headers", "*.h")) +# info related to a fat framework for a single sysroot (e.g., iphoneos) +# a fat framework contains one or more frameworks for individual archs (e.g., arm64) +@dataclass +class SysrootFrameworkInfo: + framework_dir: pathlib.Path + framework_info_file: pathlib.Path + + +# Assemble fat framework for all archs of a single sysroot +# For example, arm64 and x86_64 for iphonesimulator +def _assemble_fat_framework_for_sysroot( + intermediates_dir: pathlib.Path, sysroot: str, sysroot_arch_framework_infos: list[SysrootArchFrameworkInfo] +) -> SysrootFrameworkInfo: + assert len(sysroot_arch_framework_infos) > 0, "There must be at least one sysroot arch framework." + + # paths of the onnxruntime libraries for different archs + ort_libs = [(info.framework_dir / "onnxruntime") for info in sysroot_arch_framework_infos] + + # We only need to copy Info.plist, framework_info.json, and headers once since they are the same + info_plist_path = sysroot_arch_framework_infos[0].info_plist_file + framework_info_path = sysroot_arch_framework_infos[0].framework_info_file + header_paths = (sysroot_arch_framework_infos[0].framework_dir / "Headers").glob("*.h") # manually create the fat framework - framework_dir = os.path.join(intermediates_dir, "frameworks", sysroot, "onnxruntime.framework") + framework_dir = intermediates_dir / "frameworks" / sysroot / "onnxruntime.framework" # remove the existing framework if any - if os.path.exists(framework_dir): + if framework_dir.exists(): shutil.rmtree(framework_dir) - pathlib.Path(framework_dir).mkdir(parents=True, exist_ok=True) - - # copy the Info.plist, framework_info.json, and header files + framework_dir.mkdir(parents=True, exist_ok=True) # macos requires different framework structure: # https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html - if sysroot == "macosx" or sysroot == "macabi": - # create headers and resources directory - header_dir = os.path.join(framework_dir, "Versions", "A", "Headers") - resource_dir = os.path.join(framework_dir, "Versions", "A", "Resources") - pathlib.Path(header_dir).mkdir(parents=True, exist_ok=True) - pathlib.Path(resource_dir).mkdir(parents=True, exist_ok=True) - - shutil.copy(info_plist_path, resource_dir) - shutil.copy(framework_info_path, os.path.dirname(framework_dir)) - - for _header in headers: - shutil.copy(_header, header_dir) - - # use lipo to create a fat ort library - lipo_command = ["lipo", "-create"] - lipo_command += ort_libs - lipo_command += ["-output", os.path.join(framework_dir, "Versions", "A", "onnxruntime")] - subprocess.run(lipo_command, shell=False, check=True) - - # create the symbolic link - pathlib.Path(os.path.join(framework_dir, "Versions", "Current")).symlink_to("A", target_is_directory=True) - pathlib.Path(os.path.join(framework_dir, "Headers")).symlink_to( - "Versions/Current/Headers", target_is_directory=True - ) - pathlib.Path(os.path.join(framework_dir, "Resources")).symlink_to( - "Versions/Current/Resources", target_is_directory=True - ) - pathlib.Path(os.path.join(framework_dir, "onnxruntime")).symlink_to("Versions/Current/onnxruntime") + use_macos_framework_structure = sysroot == "macosx" or sysroot == "macabi" + if use_macos_framework_structure: + dst_header_dir = framework_dir / "Versions" / "A" / "Headers" + dst_resource_dir = framework_dir / "Versions" / "A" / "Resources" + dst_header_dir.mkdir(parents=True, exist_ok=True) + dst_resource_dir.mkdir(parents=True, exist_ok=True) + + dst_info_plist_path = dst_resource_dir / info_plist_path.name + dst_framework_info_path = framework_dir.parent / framework_info_path.name + + dst_fat_library_path = framework_dir / "Versions" / "A" / "onnxruntime" else: - shutil.copy(info_plist_path, framework_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) + dst_header_dir = framework_dir / "Headers" + dst_header_dir.mkdir(parents=True, exist_ok=True) - for _header in headers: - shutil.copy(_header, header_dir) + dst_info_plist_path = framework_dir / info_plist_path.name + dst_framework_info_path = framework_dir.parent / framework_info_path.name - # use lipo to create a fat ort library - lipo_command = ["lipo", "-create"] - lipo_command += ort_libs - lipo_command += ["-output", os.path.join(framework_dir, "onnxruntime")] - subprocess.run(lipo_command, shell=False, check=True) + dst_fat_library_path = framework_dir / "onnxruntime" - return framework_dir + # copy the Info.plist, framework_info.json, and header files + shutil.copy(info_plist_path, dst_info_plist_path) + shutil.copy(framework_info_path, dst_framework_info_path) + + for header_path in header_paths: + shutil.copy(header_path, dst_header_dir) + + # use lipo to create a fat ort library + lipo_command = ["lipo", "-create"] + lipo_command += [str(lib) for lib in ort_libs] + lipo_command += ["-output", str(dst_fat_library_path)] + subprocess.run(lipo_command, shell=False, check=True) + + if use_macos_framework_structure: + # create additional symbolic links + (framework_dir / "Versions" / "Current").symlink_to("A", target_is_directory=True) + (framework_dir / "Headers").symlink_to("Versions/Current/Headers", target_is_directory=True) + (framework_dir / "Resources").symlink_to("Versions/Current/Resources", target_is_directory=True) + (framework_dir / "onnxruntime").symlink_to("Versions/Current/onnxruntime") + + return SysrootFrameworkInfo( + framework_dir=framework_dir, + framework_info_file=dst_framework_info_path, + ) -def _merge_framework_info_files(files, output_file): +def _merge_framework_info_files(files: list[pathlib.Path], output_file: pathlib.Path): merged_data = {} for file in files: @@ -153,68 +240,37 @@ def _merge_framework_info_files(files, output_file): json.dump(merged_data, f, indent=2) -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 - - # build framework for individual sysroot - framework_dirs = [] - framework_info_files_to_merge = [] - public_headers_path = "" - for sysroot in build_settings["build_osx_archs"]: - base_build_command = ( - [sys.executable, BUILD_PY] - + build_settings["build_params"]["base"] - + build_settings["build_params"][sysroot] - + ["--config=" + build_config] - ) - - if args.include_ops_by_config is not None: - base_build_command += ["--include_ops_by_config=" + str(args.include_ops_by_config.resolve())] - - if args.path_to_protoc_exe is not None: - base_build_command += ["--path_to_protoc_exe=" + str(args.path_to_protoc_exe.resolve())] - - framework_dir = _build_for_apple_sysroot( - build_config, - intermediates_dir, - base_build_command, - sysroot, - build_settings["build_osx_archs"][sysroot], - args.build_dynamic_framework, - ) - framework_dirs.append(framework_dir) +def _assemble_xcframework(build_dir: pathlib.Path, sysroot_framework_infos: list[SysrootFrameworkInfo]) -> pathlib.Path: + assert len(sysroot_framework_infos) > 0, "There must be at least one sysroot fat framework." - curr_framework_info_path = os.path.join(os.path.dirname(framework_dir), "framework_info.json") - framework_info_files_to_merge.append(curr_framework_info_path) + framework_dirs = [info.framework_dir for info in sysroot_framework_infos] + framework_info_files_to_merge = [info.framework_info_file for info in sysroot_framework_infos] - # headers for each sysroot are the same, pick one of them - if not public_headers_path: - public_headers_path = os.path.join(os.path.dirname(framework_dir), "onnxruntime.framework", "Headers") + # headers for each sysroot are the same, pick the first one + public_headers_path = sysroot_framework_infos[0].framework_dir / "Headers" - # create the folder for xcframework and copy the LICENSE and framework_info.json 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, symlinks=True) - _merge_framework_info_files(framework_info_files_to_merge, os.path.join(build_dir, "xcframework_info.json")) + # create the output folder for the xcframework and copy the LICENSE and header files and generate the + # xcframework_info.json file + output_dir = build_dir / "framework_out" + output_dir.mkdir(parents=True, exist_ok=True) + shutil.copy(REPO_DIR / "LICENSE", output_dir) + shutil.copytree(public_headers_path, output_dir / "Headers", dirs_exist_ok=True, symlinks=True) + _merge_framework_info_files(framework_info_files_to_merge, build_dir / "xcframework_info.json") # remove existing xcframework if any - xcframework_path = os.path.join(xcframework_dir, "onnxruntime.xcframework") + xcframework_path = output_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] + build_xcframework_cmd = ["xcrun", "xcodebuild", "-create-xcframework", "-output", str(xcframework_path)] for framework_dir in framework_dirs: - build_xcframework_cmd.extend(["-framework", framework_dir]) + build_xcframework_cmd.extend(["-framework", str(framework_dir)]) subprocess.run(build_xcframework_cmd, shell=False, check=True, cwd=REPO_DIR) + return xcframework_path + def parse_args(): parser = argparse.ArgumentParser( @@ -230,7 +286,7 @@ def parse_args(): parser.add_argument( "--build_dir", type=pathlib.Path, - default=os.path.join(REPO_DIR, "build/apple_framework"), + default=(REPO_DIR / "build" / "apple_framework"), help="Provide the root directory for build output", ) @@ -254,11 +310,39 @@ def parse_args(): help="Build Dynamic Framework (default is build static framework).", ) + parser.add_argument("--path_to_protoc_exe", type=pathlib.Path, help="Path to protoc exe.") + parser.add_argument( "build_settings_file", type=pathlib.Path, help="Provide the file contains settings for building iOS framework" ) - parser.add_argument("--path_to_protoc_exe", type=pathlib.Path, help="Path to protoc exe.") + mode_group = parser.add_mutually_exclusive_group() + + mode_group.add_argument( + "--only_build_single_sysroot_arch_framework", + nargs=2, + metavar=("sysroot", "arch"), + help="Only build the specified sysroot/arch framework. E.g., sysroot = iphoneos, arch = arm64. " + "This can be used to split up the builds between different invocations of this script. " + "The sysroot and arch combination should be one from the build settings file.", + ) + + mode_group.add_argument( + "--only_build_sysroot_arch_framework_by_partition", + metavar="partition_id", + help="Only build part of all necessary sysroot/arch framework(s). " + "This can be used to split up the builds between different invocations of this script. " + "Specify the partition ID as: <1-based partition index>/, e.g., 1/3.", + ) + + mode_group.add_argument( + "--only_assemble_xcframework", + action="store_true", + help="Only assemble the xcframework (and intermediate fat frameworks) from the sysroot/arch frameworks. " + "This mode requires the necessary sysroot/arch frameworks to already be present in the build directory, " + "as if this script were previously invoked with one of `--only_build_single_sysroot_arch_framework` or " + "`--only_build_sysroot_arch_framework_by_partition` and the same `--build_dir` value.", + ) args = parser.parse_args() @@ -275,7 +359,86 @@ def parse_args(): def main(): args = parse_args() - _build_package(args) + + build_settings_file = args.build_settings_file.resolve() + build_settings = _parse_build_settings(build_settings_file) + + build_dir = args.build_dir.resolve() + build_config = args.config + + all_sysroot_arch_pairs = _get_sysroot_arch_pairs_from_build_settings(build_settings) + + # default to building frameworks and assembling xcframework + do_sysroot_arch_framework_build = do_xcframework_assembly = True + + # mode options may modify the default behavior + if ( + args.only_build_single_sysroot_arch_framework is not None + or args.only_build_sysroot_arch_framework_by_partition is not None + ): + do_xcframework_assembly = False + if args.only_assemble_xcframework: + do_sysroot_arch_framework_build = False + + # directory for intermediate build files + intermediates_dir = build_dir / "intermediates" + intermediates_dir.mkdir(parents=True, exist_ok=True) + + sysroot_to_sysroot_arch_framework_infos: dict[str, list[SysrootArchFrameworkInfo]] = {} + + if do_sysroot_arch_framework_build: + # build sysroot/arch frameworks + sysroot_arch_pairs_to_build = _filter_sysroot_arch_pairs(all_sysroot_arch_pairs, args) + + # common build command trailing args + build_command_trailing_args = [f"--config={build_config}"] + if args.include_ops_by_config is not None: + build_command_trailing_args += [f"--include_ops_by_config={args.include_ops_by_config.resolve()}"] + if args.path_to_protoc_exe is not None: + build_command_trailing_args += [f"--path_to_protoc_exe={args.path_to_protoc_exe.resolve()}"] + + for sysroot, arch in sysroot_arch_pairs_to_build: + infos_for_sysroot = sysroot_to_sysroot_arch_framework_infos.setdefault(sysroot, []) + base_build_command = ( + [sys.executable, BUILD_PY] + + build_settings["build_params"].get("base", []) + + build_settings["build_params"].get(sysroot, []) + + build_command_trailing_args + ) + + info = _find_or_build_sysroot_arch_framework( + build_config, intermediates_dir, base_build_command, sysroot, arch, args.build_dynamic_framework + ) + infos_for_sysroot.append(info) + + print(f"Built sysroot/arch framework for {sysroot}/{arch}: {info.framework_dir}") + + else: + # do not build sysroot/arch frameworks, but look for existing ones + for sysroot, arch in all_sysroot_arch_pairs: + infos_for_sysroot = sysroot_to_sysroot_arch_framework_infos.setdefault(sysroot, []) + base_build_command = None # do not build anything + info = _find_or_build_sysroot_arch_framework( + build_config, intermediates_dir, base_build_command, sysroot, arch, args.build_dynamic_framework + ) + infos_for_sysroot.append(info) + + print(f"Found existing sysroot/arch framework for {sysroot}/{arch}: {info.framework_dir}") + + if do_xcframework_assembly: + sysroot_framework_infos: list[SysrootFrameworkInfo] = [] + + # assemble fat frameworks + for sysroot, sysroot_arch_framework_infos in sysroot_to_sysroot_arch_framework_infos.items(): + sysroot_framework_info = _assemble_fat_framework_for_sysroot( + intermediates_dir, sysroot, sysroot_arch_framework_infos + ) + sysroot_framework_infos.append(sysroot_framework_info) + + # assemble xcframework + xcframework_dir = _assemble_xcframework(build_dir, sysroot_framework_infos) + + print(f"Assembled xcframework: {xcframework_dir}") if __name__ == "__main__": From ea393f1dca95240f1bbf46d90d37d132c633e21d Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:14:16 -0800 Subject: [PATCH 02/24] pipeline experiment --- .../stages/mac-ios-packaging-build-stage.yml | 414 ++++++++++-------- 1 file changed, 231 insertions(+), 183 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 7d6e272533696..33e8769b0a687 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -9,205 +9,253 @@ parameters: stages: - stage: IosPackaging_Build dependsOn: [] + + variables: + # Note: Keep the Xcode version and iOS simulator version compatible. + # Check the table here to see what iOS simulator versions are supported by a particular Xcode version: + # https://developer.apple.com/support/xcode/ + xcodeVersion: "16.2" + iosSimulatorRuntimeVersion: "18.2" + buildSettingsFile: "tools/ci_build/github/apple/default_full_apple_framework_build_settings.json" + cPodName: onnxruntime-c + objcPodName: onnxruntime-objc + jobs: - - job: - displayName: "Build iOS package" - - variables: - # Note: Keep the Xcode version and iOS simulator version compatible. - # Check the table here to see what iOS simulator versions are supported by a particular Xcode version: - # https://developer.apple.com/support/xcode/ - xcodeVersion: "16.2" - iosSimulatorRuntimeVersion: "18.2" - buildSettingsFile: "tools/ci_build/github/apple/default_full_apple_framework_build_settings.json" - cPodName: onnxruntime-c - objcPodName: onnxruntime-objc - timeoutInMinutes: 270 - templateContext: - outputs: - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: ios_packaging_artifacts_full + - job: SetUpFrameworkBuildMatrix + displayName: "Detect sys/arch framework pairs and set up build matrix" steps: - - bash: | - set -e - - BUILD_TYPE="${{ parameters.buildType }}" - BASE_VERSION="$(cat ./VERSION_NUMBER)" - SHORT_COMMIT_HASH="$(git rev-parse --short HEAD)" - DEV_VERSION="${BASE_VERSION}-dev+$(Build.BuildNumber).${SHORT_COMMIT_HASH}" - - case "${BUILD_TYPE}" in - ("release") - VERSION="${BASE_VERSION}" ;; - ("normal") - VERSION="${DEV_VERSION}" ;; - (*) - echo "Invalid build type: ${BUILD_TYPE}"; exit 1 ;; - esac - - # Do not output ##vso[] commands with `set -x` or they may be parsed again and include a trailing quote. - set +x - echo "##vso[task.setvariable variable=ortPodVersion;]${VERSION}" - echo "ortPodVersion : ${ortPodVersion}, VERSION : ${VERSION}" - displayName: "Set common variables" - name: SetCommonVariables + - task: PythonScript@0 + name: SetVariables + inputs: + scriptSource: "inline" + script: | + import json - - script: | - if [[ -z "$(ortPodVersion)" ]]; then - echo "ORT pod version is unspecified. Make sure that the IosPackaging_SetCommonVariables stage has run." - exit 1 - fi - displayName: 'Ensure version is set' + build_settings_file_path = "${{ variables.buildSettingsFile }}" - - task: InstallAppleCertificate@2 - inputs: - certSecureFile: '$(ios_signing_certificate_name)' - certPwd: '$(ios_signing_certificate_password)' - keychain: 'temp' - deleteCert: true - displayName: 'Install ORT Mobile Test Signing Certificate' + with open(build_settings_file_path, 'r') as build_settings_file: + build_settings = json.load(build_settings_file) - - task: InstallAppleProvisioningProfile@1 - inputs: - provProfileSecureFile: '$(ios_provision_profile_name)' - removeProfile: true - displayName: 'Install ORT Mobile Test Provisioning Profile' + pair_set = set() + for sysroot, archs in build_settings["build_osx_archs"].items(): + for arch in archs: + pair_set.add((sysroot, arch)) - - template: ../setup-build-tools.yml - parameters: - host_cpu_arch: arm64 + pairs = sorted(list(pair_set)) - - template: ../use-xcode-version.yml - parameters: - xcodeVersion: $(xcodeVersion) + job_matrix = {} - - script: | - pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt - displayName: "Install Python requirements" + for sysroot, arch in pairs: + identifier = f"{sysroot}_{arch}" + job_matrix[identifier] = { + "sysroot": sysroot, + "arch": arch, + } - # create and test mobile pods - - script: | - python tools/ci_build/github/apple/build_and_assemble_apple_pods.py \ - --build-dir "$(Build.BinariesDirectory)/apple_framework" \ - --staging-dir "$(Build.BinariesDirectory)/staging" \ - --pod-version "$(ortPodVersion)" \ - --test \ - --build-settings-file "${{ variables.buildSettingsFile }}" - displayName: "Build macOS/iOS framework and assemble pod package files" - env: - ORT_GET_SIMULATOR_DEVICE_INFO_REQUESTED_RUNTIME_VERSION: $(iosSimulatorRuntimeVersion) + print(f"##vso[task.setvariable variable=FrameworkBuildMatrix;isOutput=true]{json.dumps(job_matrix)}") - - script: | - python tools/ci_build/github/apple/test_apple_packages.py \ - --fail_if_cocoapods_missing \ - --framework_info_file "$(Build.BinariesDirectory)/apple_framework/xcframework_info.json" \ - --c_framework_dir "$(Build.BinariesDirectory)/apple_framework/framework_out" \ - --test_project_stage_dir "$(Build.BinariesDirectory)/app_center_test" \ - --prepare_test_project_only - displayName: "Assemble test project for App Center" - - # Xcode tasks require absolute paths because it searches for the paths and files relative to - # the root directory and not relative to the working directory - - task: Xcode@5 - inputs: - actions: 'build-for-testing' - configuration: 'Debug' - xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' - sdk: 'iphoneos' - scheme: 'ios_package_test' - signingOption: 'manual' - signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)' - provisioningProfileUuid: '$(APPLE_PROV_PROFILE_UUID)' - args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' - workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - useXcpretty: false # xcpretty can hide useful error output so we will disable it - displayName: 'Build App Center iPhone arm64 tests' + - job: FrameworkBuild + dependsOn: SetUpFrameworkBuildMatrix - - script: | - zip -r --symlinks $(Build.ArtifactStagingDirectory)/package_tests.zip ios_package_testUITests-Runner.app - workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData/Build/Products/Debug-iphoneos' - displayName: "Create .zip file of the tests" + strategy: + maxParallel: 3 + matrix: $[ dependencies.SetUpFrameworkBuildMatrix.outputs['SetVariables.FrameworkBuildMatrix'] ] + steps: - script: | - python $(Build.SourcesDirectory)/onnxruntime/test/platform/apple/generate_ipa_export_options_plist.py \ - --dest_file "exportOptions.plist" \ - --apple_team_id $(APPLE_TEAM_ID) \ - --provisioning_profile_uuid $(APPLE_PROV_PROFILE_UUID) - workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - displayName: "Generate .plist file for the .ipa file" - - # Task only generates an .xcarchive file if the plist export options are included, but does - # not produce an IPA file. - # Source code: https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/XcodeV5/xcode.ts - - task: Xcode@5 - inputs: - actions: 'archive' - xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' - packageApp: true - archivePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - exportOptions: 'plist' - exportOptionsPlist: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/exportOptions.plist' - configuration: 'Debug' - sdk: 'iphoneos' - scheme: 'ios_package_test' - args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' - workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - useXcpretty: false - displayName: 'Create archive for the .ipa file' - - # Use script step because exporting the .ipa file using the Xcode@5 task was too brittle (Xcode@5 is designed - # to handle both the .xcarchive step and the .ipa step in the same step -- ran into countless issues with signing - # and the .plist file) - - script: | - xcodebuild -exportArchive \ - -archivePath ios_package_test.xcarchive \ - -exportOptionsPlist exportOptions.plist \ - -exportPath $(Build.ArtifactStagingDirectory)/test_ipa - workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - displayName: "Create .ipa file" - - # Publish the BrowserStack artifacts first so that if the next step fails, the artifacts will still be published - # so that users can attempt to locally debug - - task: 1ES.PublishPipelineArtifact@1 - inputs: - path: $(Build.ArtifactStagingDirectory) - artifact: browserstack_test_artifacts_full - displayName: "Publish BrowserStack artifacts" + echo "Hello! sysroot is $(sysroot) and arch is $(arch)" - - script: | - set -e -x - pip install requests - python $(Build.SourcesDirectory)/tools/python/upload_and_run_browserstack_tests.py \ - --test_platform xcuitest \ - --app_path "$(Build.ArtifactStagingDirectory)/test_ipa/ios_package_test.ipa" \ - --test_path "$(Build.ArtifactStagingDirectory)/package_tests.zip" \ - --devices "iPhone 15-17" - displayName: Run E2E tests using Browserstack - workingDirectory: $(Build.BinariesDirectory)/app_center_test/apple_package_test - timeoutInMinutes: 15 - env: - BROWSERSTACK_ID: $(browserstack_username) - BROWSERSTACK_TOKEN: $(browserstack_access_key) - - script: | - set -e -x + # - job: + # displayName: "Build iOS package" - for POD_NAME in "${{ variables.cPodName}}" "${{ variables.objcPodName }}"; - do - ./tools/ci_build/github/apple/assemble_apple_packaging_artifacts.sh \ - "$(Build.BinariesDirectory)/staging" \ - "$(Build.ArtifactStagingDirectory)" \ - "${POD_NAME}" \ - "$(ortPodVersion)" - done + # timeoutInMinutes: 270 + # templateContext: + # outputs: + # - output: pipelineArtifact + # targetPath: $(Build.ArtifactStagingDirectory) + # artifactName: ios_packaging_artifacts_full - # copy over helper script for use in release pipeline - cp tools/ci_build/github/apple/package_release_tasks.py "$(Build.ArtifactStagingDirectory)" - displayName: "Assemble artifacts" + # steps: + # - bash: | + # set -e - - script: | - set -e -x - ls -R "$(Build.ArtifactStagingDirectory)" - displayName: "List staged artifacts" + # BUILD_TYPE="${{ parameters.buildType }}" + # BASE_VERSION="$(cat ./VERSION_NUMBER)" + # SHORT_COMMIT_HASH="$(git rev-parse --short HEAD)" + # DEV_VERSION="${BASE_VERSION}-dev+$(Build.BuildNumber).${SHORT_COMMIT_HASH}" + + # case "${BUILD_TYPE}" in + # ("release") + # VERSION="${BASE_VERSION}" ;; + # ("normal") + # VERSION="${DEV_VERSION}" ;; + # (*) + # echo "Invalid build type: ${BUILD_TYPE}"; exit 1 ;; + # esac + + # # Do not output ##vso[] commands with `set -x` or they may be parsed again and include a trailing quote. + # set +x + # echo "##vso[task.setvariable variable=ortPodVersion;]${VERSION}" + # echo "ortPodVersion : ${ortPodVersion}, VERSION : ${VERSION}" + # displayName: "Set common variables" + # name: SetCommonVariables + + # - script: | + # if [[ -z "$(ortPodVersion)" ]]; then + # echo "ORT pod version is unspecified. Make sure that the IosPackaging_SetCommonVariables stage has run." + # exit 1 + # fi + # displayName: 'Ensure version is set' + + # - task: InstallAppleCertificate@2 + # inputs: + # certSecureFile: '$(ios_signing_certificate_name)' + # certPwd: '$(ios_signing_certificate_password)' + # keychain: 'temp' + # deleteCert: true + # displayName: 'Install ORT Mobile Test Signing Certificate' + + # - task: InstallAppleProvisioningProfile@1 + # inputs: + # provProfileSecureFile: '$(ios_provision_profile_name)' + # removeProfile: true + # displayName: 'Install ORT Mobile Test Provisioning Profile' + + # - template: ../setup-build-tools.yml + # parameters: + # host_cpu_arch: arm64 + + # - template: ../use-xcode-version.yml + # parameters: + # xcodeVersion: $(xcodeVersion) + + # - script: | + # pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt + # displayName: "Install Python requirements" + + # # create and test mobile pods + # - script: | + # python tools/ci_build/github/apple/build_and_assemble_apple_pods.py \ + # --build-dir "$(Build.BinariesDirectory)/apple_framework" \ + # --staging-dir "$(Build.BinariesDirectory)/staging" \ + # --pod-version "$(ortPodVersion)" \ + # --test \ + # --build-settings-file "${{ variables.buildSettingsFile }}" + # displayName: "Build macOS/iOS framework and assemble pod package files" + # env: + # ORT_GET_SIMULATOR_DEVICE_INFO_REQUESTED_RUNTIME_VERSION: $(iosSimulatorRuntimeVersion) + + # - script: | + # python tools/ci_build/github/apple/test_apple_packages.py \ + # --fail_if_cocoapods_missing \ + # --framework_info_file "$(Build.BinariesDirectory)/apple_framework/xcframework_info.json" \ + # --c_framework_dir "$(Build.BinariesDirectory)/apple_framework/framework_out" \ + # --test_project_stage_dir "$(Build.BinariesDirectory)/app_center_test" \ + # --prepare_test_project_only + # displayName: "Assemble test project for App Center" + + # # Xcode tasks require absolute paths because it searches for the paths and files relative to + # # the root directory and not relative to the working directory + # - task: Xcode@5 + # inputs: + # actions: 'build-for-testing' + # configuration: 'Debug' + # xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' + # sdk: 'iphoneos' + # scheme: 'ios_package_test' + # signingOption: 'manual' + # signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)' + # provisioningProfileUuid: '$(APPLE_PROV_PROFILE_UUID)' + # args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' + # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + # useXcpretty: false # xcpretty can hide useful error output so we will disable it + # displayName: 'Build App Center iPhone arm64 tests' + + # - script: | + # zip -r --symlinks $(Build.ArtifactStagingDirectory)/package_tests.zip ios_package_testUITests-Runner.app + # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData/Build/Products/Debug-iphoneos' + # displayName: "Create .zip file of the tests" + + # - script: | + # python $(Build.SourcesDirectory)/onnxruntime/test/platform/apple/generate_ipa_export_options_plist.py \ + # --dest_file "exportOptions.plist" \ + # --apple_team_id $(APPLE_TEAM_ID) \ + # --provisioning_profile_uuid $(APPLE_PROV_PROFILE_UUID) + # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + # displayName: "Generate .plist file for the .ipa file" + + # # Task only generates an .xcarchive file if the plist export options are included, but does + # # not produce an IPA file. + # # Source code: https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/XcodeV5/xcode.ts + # - task: Xcode@5 + # inputs: + # actions: 'archive' + # xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' + # packageApp: true + # archivePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + # exportOptions: 'plist' + # exportOptionsPlist: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/exportOptions.plist' + # configuration: 'Debug' + # sdk: 'iphoneos' + # scheme: 'ios_package_test' + # args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' + # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + # useXcpretty: false + # displayName: 'Create archive for the .ipa file' + + # # Use script step because exporting the .ipa file using the Xcode@5 task was too brittle (Xcode@5 is designed + # # to handle both the .xcarchive step and the .ipa step in the same step -- ran into countless issues with signing + # # and the .plist file) + # - script: | + # xcodebuild -exportArchive \ + # -archivePath ios_package_test.xcarchive \ + # -exportOptionsPlist exportOptions.plist \ + # -exportPath $(Build.ArtifactStagingDirectory)/test_ipa + # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + # displayName: "Create .ipa file" + + # # Publish the BrowserStack artifacts first so that if the next step fails, the artifacts will still be published + # # so that users can attempt to locally debug + # - task: 1ES.PublishPipelineArtifact@1 + # inputs: + # path: $(Build.ArtifactStagingDirectory) + # artifact: browserstack_test_artifacts_full + # displayName: "Publish BrowserStack artifacts" + + # - script: | + # set -e -x + # pip install requests + # python $(Build.SourcesDirectory)/tools/python/upload_and_run_browserstack_tests.py \ + # --test_platform xcuitest \ + # --app_path "$(Build.ArtifactStagingDirectory)/test_ipa/ios_package_test.ipa" \ + # --test_path "$(Build.ArtifactStagingDirectory)/package_tests.zip" \ + # --devices "iPhone 15-17" + # displayName: Run E2E tests using Browserstack + # workingDirectory: $(Build.BinariesDirectory)/app_center_test/apple_package_test + # timeoutInMinutes: 15 + # env: + # BROWSERSTACK_ID: $(browserstack_username) + # BROWSERSTACK_TOKEN: $(browserstack_access_key) + + # - script: | + # set -e -x + + # for POD_NAME in "${{ variables.cPodName}}" "${{ variables.objcPodName }}"; + # do + # ./tools/ci_build/github/apple/assemble_apple_packaging_artifacts.sh \ + # "$(Build.BinariesDirectory)/staging" \ + # "$(Build.ArtifactStagingDirectory)" \ + # "${POD_NAME}" \ + # "$(ortPodVersion)" + # done + + # # copy over helper script for use in release pipeline + # cp tools/ci_build/github/apple/package_release_tasks.py "$(Build.ArtifactStagingDirectory)" + # displayName: "Assemble artifacts" + + # - script: | + # set -e -x + # ls -R "$(Build.ArtifactStagingDirectory)" + # displayName: "List staged artifacts" From 2fa36a5f26ee7a5694dc2d9dd2efed8f4dda1c96 Mon Sep 17 00:00:00 2001 From: Edward Chen <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:35:19 -0800 Subject: [PATCH 03/24] disable checkout, print job name --- .../templates/stages/mac-ios-packaging-build-stage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 33e8769b0a687..7b07861cc6aa1 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -63,8 +63,10 @@ stages: matrix: $[ dependencies.SetUpFrameworkBuildMatrix.outputs['SetVariables.FrameworkBuildMatrix'] ] steps: + - checkout: none - script: | echo "Hello! sysroot is $(sysroot) and arch is $(arch)" + echo "job name is $(System.JobName)" # - job: From e88cf636f921b893fa9e458759f5852d74607bd7 Mon Sep 17 00:00:00 2001 From: Edward Chen <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:43:52 -0800 Subject: [PATCH 04/24] save job names and try to depend on them later --- .../stages/mac-ios-packaging-build-stage.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 7b07861cc6aa1..a01ef9fc4d23a 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -46,6 +46,8 @@ stages: job_matrix = {} + job_names = [] + for sysroot, arch in pairs: identifier = f"{sysroot}_{arch}" job_matrix[identifier] = { @@ -53,7 +55,10 @@ stages: "arch": arch, } + job_names.append(identifier) + print(f"##vso[task.setvariable variable=FrameworkBuildMatrix;isOutput=true]{json.dumps(job_matrix)}") + print(f"##vso[task.setvariable variable=FrameworkBuildJobNames;isOutput=true]{json.dumps(job_names)}") - job: FrameworkBuild dependsOn: SetUpFrameworkBuildMatrix @@ -68,6 +73,13 @@ stages: echo "Hello! sysroot is $(sysroot) and arch is $(arch)" echo "job name is $(System.JobName)" + - job: AfterFrameworkBuilds + dependsOn: $[ dependencies.SetUpFrameworkBuildMatrix.outputs['SetVariables.FrameworkBuildJobNames'] ] + + steps: + - checkout: none + - script: | + echo "Hello! this should be after all the framework build jobs have completed!" # - job: # displayName: "Build iOS package" From 6aa8cf2eebeb008bad90e3873d6cb90ecf39522c Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:31:52 -0800 Subject: [PATCH 05/24] add build_settings_utils.py, move helpers there --- .../github/apple/build_apple_framework.py | 37 ++-------- .../github/apple/build_settings_utils.py | 74 +++++++++++++++++++ 2 files changed, 81 insertions(+), 30 deletions(-) create mode 100644 tools/ci_build/github/apple/build_settings_utils.py diff --git a/tools/ci_build/github/apple/build_apple_framework.py b/tools/ci_build/github/apple/build_apple_framework.py index b4790a051450a..9887b50401b76 100644 --- a/tools/ci_build/github/apple/build_apple_framework.py +++ b/tools/ci_build/github/apple/build_apple_framework.py @@ -14,35 +14,12 @@ import sys from dataclasses import dataclass +from build_settings_utils import parse_build_settings_file, get_sysroot_arch_pairs, get_build_params + SCRIPT_DIR = pathlib.Path(__file__).parent.resolve() REPO_DIR = SCRIPT_DIR.parents[3] BUILD_PY = REPO_DIR / "tools" / "ci_build" / "build.py" -# We by default will build below 3 archs -DEFAULT_BUILD_OSX_ARCHS = { - "iphoneos": ["arm64"], - "iphonesimulator": ["arm64", "x86_64"], -} - - -def _parse_build_settings(build_settings_file: pathlib.Path) -> dict: - with open(build_settings_file) as f: - build_settings_data = json.load(f) - - build_settings = {} - build_settings["build_osx_archs"] = build_settings_data.get("build_osx_archs", DEFAULT_BUILD_OSX_ARCHS) - build_settings["build_params"] = build_settings_data.get("build_params", {}) - - return build_settings - - -def _get_sysroot_arch_pairs_from_build_settings(build_settings) -> list[tuple[str, str]]: - pair_set: set[tuple[str, str]] = set() - for sysroot, archs in build_settings["build_osx_archs"].items(): - for arch in archs: - pair_set.add((sysroot, arch)) - return sorted(list(pair_set)) - def _filter_sysroot_arch_pairs( all_sysroot_arch_pairs: list[tuple[str, str]], @@ -55,7 +32,7 @@ def _filter_sysroot_arch_pairs( ) if specified_sysroot_arch_pair not in all_sysroot_arch_pairs: raise ValueError( - f"Sysroot/arch pair is not present in build settings file. " + "Sysroot/arch pair is not present in build settings file. " f"Specified: {specified_sysroot_arch_pair}, available: {all_sysroot_arch_pairs}" ) @@ -361,12 +338,12 @@ def main(): args = parse_args() build_settings_file = args.build_settings_file.resolve() - build_settings = _parse_build_settings(build_settings_file) + build_settings = parse_build_settings_file(build_settings_file) build_dir = args.build_dir.resolve() build_config = args.config - all_sysroot_arch_pairs = _get_sysroot_arch_pairs_from_build_settings(build_settings) + all_sysroot_arch_pairs = get_sysroot_arch_pairs(build_settings) # default to building frameworks and assembling xcframework do_sysroot_arch_framework_build = do_xcframework_assembly = True @@ -401,8 +378,8 @@ def main(): infos_for_sysroot = sysroot_to_sysroot_arch_framework_infos.setdefault(sysroot, []) base_build_command = ( [sys.executable, BUILD_PY] - + build_settings["build_params"].get("base", []) - + build_settings["build_params"].get(sysroot, []) + + get_build_params("base") + + get_build_params(sysroot) + build_command_trailing_args ) diff --git a/tools/ci_build/github/apple/build_settings_utils.py b/tools/ci_build/github/apple/build_settings_utils.py new file mode 100644 index 0000000000000..b17cbd7368e2e --- /dev/null +++ b/tools/ci_build/github/apple/build_settings_utils.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json +import pathlib + +_DEFAULT_BUILD_SYSROOT_ARCHS = { + "iphoneos": ["arm64"], + "iphonesimulator": ["arm64", "x86_64"], +} + +def parse_build_settings_file(build_settings_file: pathlib.Path) -> dict: + ''' + Parses the provided build settings file into a build settings dict. + + :param build_settings_file: The build settings file path. + :type build_settings_file: pathlib.Path + :return: The build settings dict. + :rtype: dict[Any, Any] + ''' + with open(build_settings_file) as f: + build_settings_data = json.load(f) + + # validate that `input` is a dict[str, list[str]] + def validate_str_to_str_list_dict(input: dict[str, list[str]]): + assert isinstance(input, dict), f"input is not a dict: {input}" + for key, value in input.items(): + assert isinstance(key, str), f"key is not a string: {key}" + assert isinstance(value, list), f"value is not a list: {value}" + for value_element in value: + assert isinstance(value_element, str), f"list element is not a string: {value_element}" + + build_settings = {} + + build_osx_archs = build_settings_data.get("build_osx_archs", _DEFAULT_BUILD_SYSROOT_ARCHS) + validate_str_to_str_list_dict(build_osx_archs) + build_settings["build_osx_archs"] = build_osx_archs + + build_params = build_settings_data.get("build_params", {}) + validate_str_to_str_list_dict(build_params) + build_settings["build_params"] = build_params + + return build_settings + + +def get_sysroot_arch_pairs(build_settings: dict) -> list[tuple[str, str]]: + ''' + Gets all specified sysroot/arch pairs. + + :param build_settings: The build settings dict. + :type build_settings: dict + :return: A list of (sysroot, arch) tuples. + :rtype: list[tuple[str, str]] + ''' + pair_set: set[tuple[str, str]] = set() + for sysroot, archs in build_settings["build_osx_archs"].items(): + for arch in archs: + pair_set.add((sysroot, arch)) + + return sorted(list(pair_set)) + + +def get_build_params(build_settings: dict, sysroot: str) -> list[str]: + ''' + Returns the build params associated with given `sysroot`. + The special `sysroot` value "base" may be used to get the base build params. + + :param build_settings: The build settings dict. + :type build_settings: dict + :param sysroot: The specified sysroot. + :type sysroot: str + :return: The build params associated with `sysroot`, if any, or an empty list. + :rtype: list[str] + ''' + return build_settings["build_params"].get(sysroot, []) From a8c7d28a832c93dbea4dea8604bde1a1fb9f29cd Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:26:41 -0800 Subject: [PATCH 06/24] pipeline artifact testing... --- .../stages/mac-ios-packaging-build-stage.yml | 126 ++++++++++++------ 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index a01ef9fc4d23a..82aa8602b7e65 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -6,23 +6,33 @@ parameters: - normal default: normal + # Note: Keep the Xcode version and iOS simulator version compatible. + # Check the table here to see what iOS simulator versions are supported by a particular Xcode version: + # https://developer.apple.com/support/xcode/ + +- name: xcodeVersion + type: string + default: "16.2" + +- name: iosSimulatorRuntimeVersion + type: string + default: "18.2" + +- name: buildSettingsFile + type: string + default: "tools/ci_build/github/apple/default_full_apple_framework_build_settings.json" + +- name: podNamePrefix + type: string + default: "onnxruntime" + stages: -- stage: IosPackaging_Build +- stage: BuildFrameworks dependsOn: [] - variables: - # Note: Keep the Xcode version and iOS simulator version compatible. - # Check the table here to see what iOS simulator versions are supported by a particular Xcode version: - # https://developer.apple.com/support/xcode/ - xcodeVersion: "16.2" - iosSimulatorRuntimeVersion: "18.2" - buildSettingsFile: "tools/ci_build/github/apple/default_full_apple_framework_build_settings.json" - cPodName: onnxruntime-c - objcPodName: onnxruntime-objc - jobs: - - job: SetUpFrameworkBuildMatrix - displayName: "Detect sys/arch framework pairs and set up build matrix" + - job: SetUpBuildFrameworkMatrix + displayName: "Set up sysroot/arch framework build matrix" steps: - task: PythonScript@0 @@ -31,24 +41,19 @@ stages: scriptSource: "inline" script: | import json + import sys - build_settings_file_path = "${{ variables.buildSettingsFile }}" + utils_path = "$(Build.SourcesDirectory)/tools/ci_build/github/apple" + sys.path.insert(0, utils_path) - with open(build_settings_file_path, 'r') as build_settings_file: - build_settings = json.load(build_settings_file) + from build_settings_utils import parse_build_settings_file, get_sysroot_arch_pairs - pair_set = set() - for sysroot, archs in build_settings["build_osx_archs"].items(): - for arch in archs: - pair_set.add((sysroot, arch)) - - pairs = sorted(list(pair_set)) + build_settings_file = "${{ parameters.buildSettingsFile }}" + build_settings = parse_build_settings_file(build_settings_file) + sysroot_arch_pairs = get_sysroot_arch_pairs(build_settings) job_matrix = {} - - job_names = [] - - for sysroot, arch in pairs: + for sysroot, arch in sysroot_arch_pairs: identifier = f"{sysroot}_{arch}" job_matrix[identifier] = { "sysroot": sysroot, @@ -57,33 +62,72 @@ stages: job_names.append(identifier) - print(f"##vso[task.setvariable variable=FrameworkBuildMatrix;isOutput=true]{json.dumps(job_matrix)}") - print(f"##vso[task.setvariable variable=FrameworkBuildJobNames;isOutput=true]{json.dumps(job_names)}") + print(f"##vso[task.setvariable variable=BuildFrameworkMatrix;isOutput=true]{json.dumps(job_matrix)}") - - job: FrameworkBuild - dependsOn: SetUpFrameworkBuildMatrix + - job: BuildFramework + dependsOn: SetUpBuildFrameworkMatrix strategy: - maxParallel: 3 - matrix: $[ dependencies.SetUpFrameworkBuildMatrix.outputs['SetVariables.FrameworkBuildMatrix'] ] + maxParallel: 8 + matrix: $[ dependencies.SetUpBuildFrameworkMatrix.outputs['SetVariables.BuildFrameworkMatrix'] ] steps: - - checkout: none + # - template: ../setup-build-tools.yml + # parameters: + # host_cpu_arch: arm64 + + # - template: ../use-xcode-version.yml + # parameters: + # xcodeVersion: ${{ parameters.xcodeVersion }} + + # - script: | + # pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt + # displayName: "Install Python requirements" + + # - script: | + # python tools/ci_build/github/apple/build_apple_framework.py \ + # --build_dir "$(Build.BinariesDirectory)/apple_framework" \ + # --only_build_single_sysroot_arch_framework "$(sysroot)" "$(arch)" \ + # "${{ parameters.buildSettingsFile }}" + # displayName: "Build framework for $(sysroot)/$(arch)" + - script: | - echo "Hello! sysroot is $(sysroot) and arch is $(arch)" - echo "job name is $(System.JobName)" + INTERMEDIATES_DIR="$(Build.ArtifactStagingDirectory)/intermediates" + mkdir -p "${INTERMEDIATES_DIR}" + + echo "hello from build with sysroot $(sysroot) and arch $(arch)" > "${INTERMEDIATES_DIR}/out.txt" + displayName: Generate output file + + - publish: $(Build.ArtifactStagingDirectory) + artifact: framework_$(sysroot)_$(arch) - - job: AfterFrameworkBuilds - dependsOn: $[ dependencies.SetUpFrameworkBuildMatrix.outputs['SetVariables.FrameworkBuildJobNames'] ] +- stage: AfterBuildFrameworks + dependsOn: BuildFrameworks + jobs: + - job: UseArtifacts steps: - checkout: none + + - task: DownloadPipelineArtifact@2 + inputs: + itemPattern: framework_*/** + targetPath: $(Build.BinariesDirectory) + - script: | - echo "Hello! this should be after all the framework build jobs have completed!" + find "$(Build.BinariesDirectory)" + displayName: Show downloaded artifacts + +# - stage: IosPackaging_Build +# dependsOn: [] # - job: # displayName: "Build iOS package" + # variables: + # cPodName: ${{ parameters.podNamePrefix }}-c + # objcPodName: ${{ parameters.podNamePrefix }}-objc + # timeoutInMinutes: 270 # templateContext: # outputs: @@ -143,7 +187,7 @@ stages: # - template: ../use-xcode-version.yml # parameters: - # xcodeVersion: $(xcodeVersion) + # xcodeVersion: ${{ parameters.xcodeVersion }} # - script: | # pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt @@ -156,10 +200,10 @@ stages: # --staging-dir "$(Build.BinariesDirectory)/staging" \ # --pod-version "$(ortPodVersion)" \ # --test \ - # --build-settings-file "${{ variables.buildSettingsFile }}" + # --build-settings-file "${{ parameters.buildSettingsFile }}" # displayName: "Build macOS/iOS framework and assemble pod package files" # env: - # ORT_GET_SIMULATOR_DEVICE_INFO_REQUESTED_RUNTIME_VERSION: $(iosSimulatorRuntimeVersion) + # ORT_GET_SIMULATOR_DEVICE_INFO_REQUESTED_RUNTIME_VERSION: ${{ parameters.iosSimulatorRuntimeVersion }} # - script: | # python tools/ci_build/github/apple/test_apple_packages.py \ From 17e95ca656ee6d2dd55a5622321d826c10cbe287 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:46:25 -0800 Subject: [PATCH 07/24] use templateContext for publishing artifact --- .../templates/stages/mac-ios-packaging-build-stage.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 82aa8602b7e65..3c12e56091667 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -71,6 +71,12 @@ stages: maxParallel: 8 matrix: $[ dependencies.SetUpBuildFrameworkMatrix.outputs['SetVariables.BuildFrameworkMatrix'] ] + templateContext: + outputs: + - output: pipelineArtifact + path: $(Build.ArtifactStagingDirectory) + artifact: framework_$(sysroot)_$(arch) + steps: # - template: ../setup-build-tools.yml # parameters: @@ -98,9 +104,6 @@ stages: echo "hello from build with sysroot $(sysroot) and arch $(arch)" > "${INTERMEDIATES_DIR}/out.txt" displayName: Generate output file - - publish: $(Build.ArtifactStagingDirectory) - artifact: framework_$(sysroot)_$(arch) - - stage: AfterBuildFrameworks dependsOn: BuildFrameworks From 80a178bcce8beb36aa48fb24ffe22fe1a2ff63e4 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:50:45 -0800 Subject: [PATCH 08/24] fix python script --- .../templates/stages/mac-ios-packaging-build-stage.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 3c12e56091667..362a2a3eb3181 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -60,8 +60,6 @@ stages: "arch": arch, } - job_names.append(identifier) - print(f"##vso[task.setvariable variable=BuildFrameworkMatrix;isOutput=true]{json.dumps(job_matrix)}") - job: BuildFramework From 6c6aaf884697fa5322c385ea02befc304127710c Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:39:29 -0800 Subject: [PATCH 09/24] missed build_settings parameter --- tools/ci_build/github/apple/build_apple_framework.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/ci_build/github/apple/build_apple_framework.py b/tools/ci_build/github/apple/build_apple_framework.py index 9887b50401b76..5bb30fd8ab629 100644 --- a/tools/ci_build/github/apple/build_apple_framework.py +++ b/tools/ci_build/github/apple/build_apple_framework.py @@ -378,8 +378,8 @@ def main(): infos_for_sysroot = sysroot_to_sysroot_arch_framework_infos.setdefault(sysroot, []) base_build_command = ( [sys.executable, BUILD_PY] - + get_build_params("base") - + get_build_params(sysroot) + + get_build_params(build_settings, "base") + + get_build_params(build_settings, sysroot) + build_command_trailing_args ) From 6a2faece473aeec7c686321cf10c223641351f6a Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:40:04 -0800 Subject: [PATCH 10/24] update --- .../stages/mac-ios-packaging-build-stage.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 362a2a3eb3181..bcadbafdffb87 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -31,8 +31,7 @@ stages: dependsOn: [] jobs: - - job: SetUpBuildFrameworkMatrix - displayName: "Set up sysroot/arch framework build matrix" + - job: SetUpBuildFrameworkJobMatrix steps: - task: PythonScript@0 @@ -60,14 +59,18 @@ stages: "arch": arch, } - print(f"##vso[task.setvariable variable=BuildFrameworkMatrix;isOutput=true]{json.dumps(job_matrix)}") + job_matrix_json = json.dumps(job_matrix) + + print(f"Build framework job matrix:\n{job_matrix_json}") + print(f"##vso[task.setvariable variable=BuildFrameworkJobMatrix;isOutput=true]{job_matrix_json}") + displayName: "Generate build framework job matrix" - job: BuildFramework - dependsOn: SetUpBuildFrameworkMatrix + dependsOn: SetUpBuildFrameworkJobMatrix strategy: maxParallel: 8 - matrix: $[ dependencies.SetUpBuildFrameworkMatrix.outputs['SetVariables.BuildFrameworkMatrix'] ] + matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] templateContext: outputs: From 071951ba09c933d0b9474efa8b57ce39006a2ac1 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:58:43 -0800 Subject: [PATCH 11/24] remove only_build_sysroot_arch_framework_by_partition option --- .../github/apple/build_apple_framework.py | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/tools/ci_build/github/apple/build_apple_framework.py b/tools/ci_build/github/apple/build_apple_framework.py index 5bb30fd8ab629..d24d31e57f7c6 100644 --- a/tools/ci_build/github/apple/build_apple_framework.py +++ b/tools/ci_build/github/apple/build_apple_framework.py @@ -8,7 +8,6 @@ import json import os import pathlib -import re import shutil import subprocess import sys @@ -38,27 +37,6 @@ def _filter_sysroot_arch_pairs( return [specified_sysroot_arch_pair] - if args.only_build_sysroot_arch_framework_by_partition is not None: - - def parse(partition_id: str) -> tuple[int, int]: - match = re.fullmatch(r"^(\d+)/(\d+)$", partition_id) - if not match: - raise ValueError( - f"Invalid partition ID: {partition_id}. Expected format is /." - ) - numerator, denominator = int(match.group(1)), int(match.group(2)) - if numerator == 0 or numerator > denominator: - raise ValueError( - f"Invalid partition ID: {partition_id}. " - "Numerator must be non-zero and not greater than denominator." - ) - return numerator, denominator - - one_based_partition_idx, num_partitions = parse(args.only_build_sysroot_arch_framework_by_partition) - partition_idx = one_based_partition_idx - 1 - - return all_sysroot_arch_pairs[partition_idx::num_partitions] - return all_sysroot_arch_pairs.copy() @@ -304,14 +282,6 @@ def parse_args(): "The sysroot and arch combination should be one from the build settings file.", ) - mode_group.add_argument( - "--only_build_sysroot_arch_framework_by_partition", - metavar="partition_id", - help="Only build part of all necessary sysroot/arch framework(s). " - "This can be used to split up the builds between different invocations of this script. " - "Specify the partition ID as: <1-based partition index>/, e.g., 1/3.", - ) - mode_group.add_argument( "--only_assemble_xcframework", action="store_true", @@ -349,10 +319,7 @@ def main(): do_sysroot_arch_framework_build = do_xcframework_assembly = True # mode options may modify the default behavior - if ( - args.only_build_single_sysroot_arch_framework is not None - or args.only_build_sysroot_arch_framework_by_partition is not None - ): + if args.only_build_single_sysroot_arch_framework is not None: do_xcframework_assembly = False if args.only_assemble_xcframework: do_sysroot_arch_framework_build = False From 66b740ad3f42fac2b0ad7dd7d1dee7c1f9594d67 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:39:17 -0800 Subject: [PATCH 12/24] add build outputs option, other fixes --- .../github/apple/build_apple_framework.py | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/tools/ci_build/github/apple/build_apple_framework.py b/tools/ci_build/github/apple/build_apple_framework.py index d24d31e57f7c6..753eeb6b734ba 100644 --- a/tools/ci_build/github/apple/build_apple_framework.py +++ b/tools/ci_build/github/apple/build_apple_framework.py @@ -13,7 +13,7 @@ import sys from dataclasses import dataclass -from build_settings_utils import parse_build_settings_file, get_sysroot_arch_pairs, get_build_params +from build_settings_utils import get_build_params, get_sysroot_arch_pairs, parse_build_settings_file SCRIPT_DIR = pathlib.Path(__file__).parent.resolve() REPO_DIR = SCRIPT_DIR.parents[3] @@ -287,8 +287,17 @@ def parse_args(): action="store_true", help="Only assemble the xcframework (and intermediate fat frameworks) from the sysroot/arch frameworks. " "This mode requires the necessary sysroot/arch frameworks to already be present in the build directory, " - "as if this script were previously invoked with one of `--only_build_single_sysroot_arch_framework` or " - "`--only_build_sysroot_arch_framework_by_partition` and the same `--build_dir` value.", + "as if this script were previously invoked with `--only_build_single_sysroot_arch_framework` and the same " + "`--build_dir` value.", + ) + + parser.add_argument( + "--record_sysroot_arch_framework_build_outputs_to_file", + type=pathlib.Path, + help="Write the sysroot/arch framework build outputs to the specified file. " + "This option is only valid when `--only_build_single_sysroot_arch_framework` is specified. " + "These output files are the files that should be preserved between split-build invocations with " + "`--only_build_single_sysroot_arch_framework` and `--only_assemble_xcframework`.", ) args = parser.parse_args() @@ -301,6 +310,15 @@ def parse_args(): if not include_ops_by_config_file.is_file(): raise FileNotFoundError(f"Include ops config file {include_ops_by_config_file} is not a file.") + if ( + args.record_sysroot_arch_framework_build_outputs_to_file is not None + and args.only_build_single_sysroot_arch_framework is None + ): + raise ValueError( + "--record_sysroot_arch_framework_build_outputs_to_file is only valid if " + "--only_build_single_sysroot_arch_framework is specified" + ) + return args @@ -341,22 +359,32 @@ def main(): if args.path_to_protoc_exe is not None: build_command_trailing_args += [f"--path_to_protoc_exe={args.path_to_protoc_exe.resolve()}"] + built_sysroot_arch_framework_infos: list[SysrootArchFrameworkInfo] = [] + for sysroot, arch in sysroot_arch_pairs_to_build: infos_for_sysroot = sysroot_to_sysroot_arch_framework_infos.setdefault(sysroot, []) - base_build_command = ( - [sys.executable, BUILD_PY] - + get_build_params(build_settings, "base") - + get_build_params(build_settings, sysroot) - + build_command_trailing_args - ) + base_build_command = [ + sys.executable, + BUILD_PY, + *get_build_params(build_settings, "base"), + *get_build_params(build_settings, sysroot), + *build_command_trailing_args, + ] info = _find_or_build_sysroot_arch_framework( build_config, intermediates_dir, base_build_command, sysroot, arch, args.build_dynamic_framework ) infos_for_sysroot.append(info) + built_sysroot_arch_framework_infos.append(info) print(f"Built sysroot/arch framework for {sysroot}/{arch}: {info.framework_dir}") + if args.record_sysroot_arch_framework_build_outputs_to_file is not None: + with open(args.record_sysroot_arch_framework_build_outputs_to_file, mode="w") as build_outputs_file: + for info in built_sysroot_arch_framework_infos: + print(info.framework_dir, info.framework_info_file, info.info_plist_file, + file=build_outputs_file, sep="\n") + else: # do not build sysroot/arch frameworks, but look for existing ones for sysroot, arch in all_sysroot_arch_pairs: From 48bba6926d458ca3ba9d3e4bd52ad28eae8664d2 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:21:56 -0800 Subject: [PATCH 13/24] update pipeline --- .../stages/mac-ios-packaging-build-stage.yml | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index bcadbafdffb87..b5e14d3817610 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -99,11 +99,17 @@ stages: # displayName: "Build framework for $(sysroot)/$(arch)" - script: | - INTERMEDIATES_DIR="$(Build.ArtifactStagingDirectory)/intermediates" + BUILD_DIR="$(Build.BinariesDirectory)/build" + INTERMEDIATES_DIR="${BUILD_DIR}/intermediates/$(sysroot)/$(arch)" mkdir -p "${INTERMEDIATES_DIR}" echo "hello from build with sysroot $(sysroot) and arch $(arch)" > "${INTERMEDIATES_DIR}/out.txt" - displayName: Generate output file + + pushd "${BUILD_DIR}" + zip --recurse-paths --symlinks build.zip . + mv build.zip $(Build.ArtifactStagingDirectory) + popd + displayName: Generate output artifact - stage: AfterBuildFrameworks dependsOn: BuildFrameworks @@ -115,12 +121,16 @@ stages: - task: DownloadPipelineArtifact@2 inputs: - itemPattern: framework_*/** - targetPath: $(Build.BinariesDirectory) + itemPattern: framework_*/build.zip + targetPath: $(Build.BinariesDirectory)/artifacts - script: | - find "$(Build.BinariesDirectory)" - displayName: Show downloaded artifacts + BUILD_DIR="$(Build.BinariesDirectory)/build" + + find "$(Build.BinariesDirectory)/artifacts" -name "build.zip" -exec unzip {} -d "${BUILD_DIR}" \; + + find "${BUILD_DIR}" + displayName: Unpack downloaded artifacts to build directory # - stage: IosPackaging_Build # dependsOn: [] From 6874a5c4c5998837b8c1db30971b911470837f01 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:22:24 -0800 Subject: [PATCH 14/24] add helper function for writing build outputs --- .../github/apple/build_apple_framework.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/tools/ci_build/github/apple/build_apple_framework.py b/tools/ci_build/github/apple/build_apple_framework.py index 753eeb6b734ba..9b9f7f6cf234f 100644 --- a/tools/ci_build/github/apple/build_apple_framework.py +++ b/tools/ci_build/github/apple/build_apple_framework.py @@ -102,6 +102,22 @@ def _find_or_build_sysroot_arch_framework( ) +def _write_sysroot_arch_framework_build_outputs_to_file( + build_dir: pathlib.Path, + built_sysroot_arch_framework_infos: list[SysrootArchFrameworkInfo], + build_outputs_file_path: pathlib.Path, +): + with open(build_outputs_file_path, mode="w") as build_outputs_file: + + def write_path(p: pathlib.Path): + print(p.resolve().relative_to(build_dir), file=build_outputs_file) + + for info in built_sysroot_arch_framework_infos: + write_path(info.framework_dir) + write_path(info.framework_info_file) + write_path(info.info_plist_file) + + # info related to a fat framework for a single sysroot (e.g., iphoneos) # a fat framework contains one or more frameworks for individual archs (e.g., arm64) @dataclass @@ -294,9 +310,9 @@ def parse_args(): parser.add_argument( "--record_sysroot_arch_framework_build_outputs_to_file", type=pathlib.Path, - help="Write the sysroot/arch framework build outputs to the specified file. " - "This option is only valid when `--only_build_single_sysroot_arch_framework` is specified. " - "These output files are the files that should be preserved between split-build invocations with " + help="If building sysroot/arch framework(s), write the build output file paths to the specified file. " + "The paths will be relative to the build directory specified by `--build_dir`. " + "These build output files are the files that should be preserved between split-build invocations with " "`--only_build_single_sysroot_arch_framework` and `--only_assemble_xcframework`.", ) @@ -310,15 +326,6 @@ def parse_args(): if not include_ops_by_config_file.is_file(): raise FileNotFoundError(f"Include ops config file {include_ops_by_config_file} is not a file.") - if ( - args.record_sysroot_arch_framework_build_outputs_to_file is not None - and args.only_build_single_sysroot_arch_framework is None - ): - raise ValueError( - "--record_sysroot_arch_framework_build_outputs_to_file is only valid if " - "--only_build_single_sysroot_arch_framework is specified" - ) - return args @@ -380,10 +387,9 @@ def main(): print(f"Built sysroot/arch framework for {sysroot}/{arch}: {info.framework_dir}") if args.record_sysroot_arch_framework_build_outputs_to_file is not None: - with open(args.record_sysroot_arch_framework_build_outputs_to_file, mode="w") as build_outputs_file: - for info in built_sysroot_arch_framework_infos: - print(info.framework_dir, info.framework_info_file, info.info_plist_file, - file=build_outputs_file, sep="\n") + _write_sysroot_arch_framework_build_outputs_to_file( + build_dir, built_sysroot_arch_framework_infos, args.record_sysroot_arch_framework_build_outputs_to_file + ) else: # do not build sysroot/arch frameworks, but look for existing ones From 8b4e6ef6f429e79954f62d8c2b704b358cba6735 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:29:47 -0800 Subject: [PATCH 15/24] packaging pipeline for real --- .../stages/mac-ios-packaging-build-stage.yml | 464 +++++++++--------- 1 file changed, 232 insertions(+), 232 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index b5e14d3817610..433d4df060908 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -72,50 +72,59 @@ stages: maxParallel: 8 matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] - templateContext: - outputs: - - output: pipelineArtifact - path: $(Build.ArtifactStagingDirectory) - artifact: framework_$(sysroot)_$(arch) - steps: - # - template: ../setup-build-tools.yml - # parameters: - # host_cpu_arch: arm64 + - template: ../setup-build-tools.yml + parameters: + host_cpu_arch: arm64 - # - template: ../use-xcode-version.yml - # parameters: - # xcodeVersion: ${{ parameters.xcodeVersion }} + - template: ../use-xcode-version.yml + parameters: + xcodeVersion: ${{ parameters.xcodeVersion }} - # - script: | - # pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt - # displayName: "Install Python requirements" + - script: | + pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt + displayName: "Install Python requirements" - # - script: | - # python tools/ci_build/github/apple/build_apple_framework.py \ - # --build_dir "$(Build.BinariesDirectory)/apple_framework" \ - # --only_build_single_sysroot_arch_framework "$(sysroot)" "$(arch)" \ - # "${{ parameters.buildSettingsFile }}" - # displayName: "Build framework for $(sysroot)/$(arch)" + - script: | + python tools/ci_build/github/apple/build_apple_framework.py \ + --build_dir "$(Build.BinariesDirectory)/build" \ + --only_build_single_sysroot_arch_framework "$(sysroot)" "$(arch)" \ + --record_sysroot_arch_framework_build_outputs_to_file "$(Build.BinariesDirectory)/build_outputs.txt" \ + "${{ parameters.buildSettingsFile }}" + displayName: "Build framework for $(sysroot)/$(arch)" - script: | - BUILD_DIR="$(Build.BinariesDirectory)/build" - INTERMEDIATES_DIR="${BUILD_DIR}/intermediates/$(sysroot)/$(arch)" - mkdir -p "${INTERMEDIATES_DIR}" + set -e - echo "hello from build with sysroot $(sysroot) and arch $(arch)" > "${INTERMEDIATES_DIR}/out.txt" + BUILD_OUTPUTS_FILE="$(Build.BinariesDirectory)/build_outputs.txt" + BUILD_DIR="$(Build.BinariesDirectory)/build" pushd "${BUILD_DIR}" - zip --recurse-paths --symlinks build.zip . - mv build.zip $(Build.ArtifactStagingDirectory) + zip \ + --recurse-paths \ + --symlinks \ + --names-stdin \ + "$(Build.ArtifactStagingDirectory)/build.zip" \ + < "${BUILD_OUTPUTS_FILE}" popd - displayName: Generate output artifact + displayName: "Create framework build archive artifact" + + - task: 1ES.PublishPipelineArtifact@1 + inputs: + path: $(Build.ArtifactStagingDirectory) + artifact: framework_$(sysroot)_$(arch) + displayName: "Publish artifact - framework_$(sysroot)_$(arch)" -- stage: AfterBuildFrameworks +- stage: AssemblePackageAndTest dependsOn: BuildFrameworks + variables: + cPodName: ${{ parameters.podNamePrefix }}-c + objcPodName: ${{ parameters.podNamePrefix }}-objc + jobs: - - job: UseArtifacts + - job: AssemblePackageAndTest + steps: - checkout: none @@ -125,209 +134,200 @@ stages: targetPath: $(Build.BinariesDirectory)/artifacts - script: | - BUILD_DIR="$(Build.BinariesDirectory)/build" + set -e + BUILD_DIR="$(Build.BinariesDirectory)/build" + mkdir -p "${BUILD_DIR}" find "$(Build.BinariesDirectory)/artifacts" -name "build.zip" -exec unzip {} -d "${BUILD_DIR}" \; find "${BUILD_DIR}" - displayName: Unpack downloaded artifacts to build directory - -# - stage: IosPackaging_Build -# dependsOn: [] - - # - job: - # displayName: "Build iOS package" - - # variables: - # cPodName: ${{ parameters.podNamePrefix }}-c - # objcPodName: ${{ parameters.podNamePrefix }}-objc - - # timeoutInMinutes: 270 - # templateContext: - # outputs: - # - output: pipelineArtifact - # targetPath: $(Build.ArtifactStagingDirectory) - # artifactName: ios_packaging_artifacts_full - - # steps: - # - bash: | - # set -e - - # BUILD_TYPE="${{ parameters.buildType }}" - # BASE_VERSION="$(cat ./VERSION_NUMBER)" - # SHORT_COMMIT_HASH="$(git rev-parse --short HEAD)" - # DEV_VERSION="${BASE_VERSION}-dev+$(Build.BuildNumber).${SHORT_COMMIT_HASH}" - - # case "${BUILD_TYPE}" in - # ("release") - # VERSION="${BASE_VERSION}" ;; - # ("normal") - # VERSION="${DEV_VERSION}" ;; - # (*) - # echo "Invalid build type: ${BUILD_TYPE}"; exit 1 ;; - # esac - - # # Do not output ##vso[] commands with `set -x` or they may be parsed again and include a trailing quote. - # set +x - # echo "##vso[task.setvariable variable=ortPodVersion;]${VERSION}" - # echo "ortPodVersion : ${ortPodVersion}, VERSION : ${VERSION}" - # displayName: "Set common variables" - # name: SetCommonVariables - - # - script: | - # if [[ -z "$(ortPodVersion)" ]]; then - # echo "ORT pod version is unspecified. Make sure that the IosPackaging_SetCommonVariables stage has run." - # exit 1 - # fi - # displayName: 'Ensure version is set' - - # - task: InstallAppleCertificate@2 - # inputs: - # certSecureFile: '$(ios_signing_certificate_name)' - # certPwd: '$(ios_signing_certificate_password)' - # keychain: 'temp' - # deleteCert: true - # displayName: 'Install ORT Mobile Test Signing Certificate' - - # - task: InstallAppleProvisioningProfile@1 - # inputs: - # provProfileSecureFile: '$(ios_provision_profile_name)' - # removeProfile: true - # displayName: 'Install ORT Mobile Test Provisioning Profile' - - # - template: ../setup-build-tools.yml - # parameters: - # host_cpu_arch: arm64 - - # - template: ../use-xcode-version.yml - # parameters: - # xcodeVersion: ${{ parameters.xcodeVersion }} - - # - script: | - # pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt - # displayName: "Install Python requirements" - - # # create and test mobile pods - # - script: | - # python tools/ci_build/github/apple/build_and_assemble_apple_pods.py \ - # --build-dir "$(Build.BinariesDirectory)/apple_framework" \ - # --staging-dir "$(Build.BinariesDirectory)/staging" \ - # --pod-version "$(ortPodVersion)" \ - # --test \ - # --build-settings-file "${{ parameters.buildSettingsFile }}" - # displayName: "Build macOS/iOS framework and assemble pod package files" - # env: - # ORT_GET_SIMULATOR_DEVICE_INFO_REQUESTED_RUNTIME_VERSION: ${{ parameters.iosSimulatorRuntimeVersion }} - - # - script: | - # python tools/ci_build/github/apple/test_apple_packages.py \ - # --fail_if_cocoapods_missing \ - # --framework_info_file "$(Build.BinariesDirectory)/apple_framework/xcframework_info.json" \ - # --c_framework_dir "$(Build.BinariesDirectory)/apple_framework/framework_out" \ - # --test_project_stage_dir "$(Build.BinariesDirectory)/app_center_test" \ - # --prepare_test_project_only - # displayName: "Assemble test project for App Center" - - # # Xcode tasks require absolute paths because it searches for the paths and files relative to - # # the root directory and not relative to the working directory - # - task: Xcode@5 - # inputs: - # actions: 'build-for-testing' - # configuration: 'Debug' - # xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' - # sdk: 'iphoneos' - # scheme: 'ios_package_test' - # signingOption: 'manual' - # signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)' - # provisioningProfileUuid: '$(APPLE_PROV_PROFILE_UUID)' - # args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' - # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - # useXcpretty: false # xcpretty can hide useful error output so we will disable it - # displayName: 'Build App Center iPhone arm64 tests' - - # - script: | - # zip -r --symlinks $(Build.ArtifactStagingDirectory)/package_tests.zip ios_package_testUITests-Runner.app - # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData/Build/Products/Debug-iphoneos' - # displayName: "Create .zip file of the tests" - - # - script: | - # python $(Build.SourcesDirectory)/onnxruntime/test/platform/apple/generate_ipa_export_options_plist.py \ - # --dest_file "exportOptions.plist" \ - # --apple_team_id $(APPLE_TEAM_ID) \ - # --provisioning_profile_uuid $(APPLE_PROV_PROFILE_UUID) - # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - # displayName: "Generate .plist file for the .ipa file" - - # # Task only generates an .xcarchive file if the plist export options are included, but does - # # not produce an IPA file. - # # Source code: https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/XcodeV5/xcode.ts - # - task: Xcode@5 - # inputs: - # actions: 'archive' - # xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' - # packageApp: true - # archivePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - # exportOptions: 'plist' - # exportOptionsPlist: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/exportOptions.plist' - # configuration: 'Debug' - # sdk: 'iphoneos' - # scheme: 'ios_package_test' - # args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' - # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - # useXcpretty: false - # displayName: 'Create archive for the .ipa file' - - # # Use script step because exporting the .ipa file using the Xcode@5 task was too brittle (Xcode@5 is designed - # # to handle both the .xcarchive step and the .ipa step in the same step -- ran into countless issues with signing - # # and the .plist file) - # - script: | - # xcodebuild -exportArchive \ - # -archivePath ios_package_test.xcarchive \ - # -exportOptionsPlist exportOptions.plist \ - # -exportPath $(Build.ArtifactStagingDirectory)/test_ipa - # workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' - # displayName: "Create .ipa file" - - # # Publish the BrowserStack artifacts first so that if the next step fails, the artifacts will still be published - # # so that users can attempt to locally debug - # - task: 1ES.PublishPipelineArtifact@1 - # inputs: - # path: $(Build.ArtifactStagingDirectory) - # artifact: browserstack_test_artifacts_full - # displayName: "Publish BrowserStack artifacts" - - # - script: | - # set -e -x - # pip install requests - # python $(Build.SourcesDirectory)/tools/python/upload_and_run_browserstack_tests.py \ - # --test_platform xcuitest \ - # --app_path "$(Build.ArtifactStagingDirectory)/test_ipa/ios_package_test.ipa" \ - # --test_path "$(Build.ArtifactStagingDirectory)/package_tests.zip" \ - # --devices "iPhone 15-17" - # displayName: Run E2E tests using Browserstack - # workingDirectory: $(Build.BinariesDirectory)/app_center_test/apple_package_test - # timeoutInMinutes: 15 - # env: - # BROWSERSTACK_ID: $(browserstack_username) - # BROWSERSTACK_TOKEN: $(browserstack_access_key) - - # - script: | - # set -e -x - - # for POD_NAME in "${{ variables.cPodName}}" "${{ variables.objcPodName }}"; - # do - # ./tools/ci_build/github/apple/assemble_apple_packaging_artifacts.sh \ - # "$(Build.BinariesDirectory)/staging" \ - # "$(Build.ArtifactStagingDirectory)" \ - # "${POD_NAME}" \ - # "$(ortPodVersion)" - # done - - # # copy over helper script for use in release pipeline - # cp tools/ci_build/github/apple/package_release_tasks.py "$(Build.ArtifactStagingDirectory)" - # displayName: "Assemble artifacts" - - # - script: | - # set -e -x - # ls -R "$(Build.ArtifactStagingDirectory)" - # displayName: "List staged artifacts" + displayName: Extract framework build archive artifacts to build directory + + - script: | + set -e + + BUILD_TYPE="${{ parameters.buildType }}" + BASE_VERSION="$(cat ./VERSION_NUMBER)" + SHORT_COMMIT_HASH="$(git rev-parse --short HEAD)" + DEV_VERSION="${BASE_VERSION}-dev+$(Build.BuildNumber).${SHORT_COMMIT_HASH}" + + case "${BUILD_TYPE}" in + ("release") + VERSION="${BASE_VERSION}" ;; + ("normal") + VERSION="${DEV_VERSION}" ;; + (*) + echo "Invalid build type: ${BUILD_TYPE}"; exit 1 ;; + esac + + # Do not output ##vso[] commands with `set -x` or they may be parsed again and include a trailing quote. + set +x + echo "##vso[task.setvariable variable=ortPodVersion;]${VERSION}" + echo "ortPodVersion : ${ortPodVersion}, VERSION : ${VERSION}" + displayName: "Set ortPodVersion variable" + + - task: InstallAppleCertificate@2 + inputs: + certSecureFile: '$(ios_signing_certificate_name)' + certPwd: '$(ios_signing_certificate_password)' + keychain: 'temp' + deleteCert: true + displayName: 'Install ORT Mobile Test Signing Certificate' + + - task: InstallAppleProvisioningProfile@1 + inputs: + provProfileSecureFile: '$(ios_provision_profile_name)' + removeProfile: true + displayName: 'Install ORT Mobile Test Provisioning Profile' + + - template: ../setup-build-tools.yml + parameters: + host_cpu_arch: arm64 + + - template: ../use-xcode-version.yml + parameters: + xcodeVersion: ${{ parameters.xcodeVersion }} + + - script: | + pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt + displayName: "Install Python requirements" + + # Assemble the xcframework and the pod packages. + # The frameworks from the BuildFrameworks stage should already be in the build directory. + - script: | + python tools/ci_build/github/apple/build_and_assemble_apple_pods.py \ + --build-dir "$(Build.BinariesDirectory)/build" \ + --staging-dir "$(Build.BinariesDirectory)/staging" \ + --pod-version "$(ortPodVersion)" \ + --test \ + --build-apple-framework-arg=--only_assemble_xcframework \ + --build-settings-file "${{ parameters.buildSettingsFile }}" + displayName: "Assemble pod package files" + env: + ORT_GET_SIMULATOR_DEVICE_INFO_REQUESTED_RUNTIME_VERSION: ${{ parameters.iosSimulatorRuntimeVersion }} + + - script: | + set -e -x + + mkdir -p "$(Build.ArtifactStagingDirectory)/package" + + for POD_NAME in "${{ variables.cPodName}}" "${{ variables.objcPodName }}"; + do + ./tools/ci_build/github/apple/assemble_apple_packaging_artifacts.sh \ + "$(Build.BinariesDirectory)/staging" \ + "$(Build.ArtifactStagingDirectory)/package" \ + "${POD_NAME}" \ + "$(ortPodVersion)" + done + + # copy over helper script for use in release pipeline + cp tools/ci_build/github/apple/package_release_tasks.py "$(Build.ArtifactStagingDirectory)/package" + displayName: "Assemble packaging artifacts" + + # Publish package artifact + - task: 1ES.PublishPipelineArtifact@1 + inputs: + path: $(Build.ArtifactStagingDirectory)/package + artifact: ios_packaging_artifacts_full + displayName: "Publish artifact - ios_packaging_artifacts_full" + + # Run more tests on the package + + - script: | + python tools/ci_build/github/apple/test_apple_packages.py \ + --fail_if_cocoapods_missing \ + --framework_info_file "$(Build.BinariesDirectory)/build/xcframework_info.json" \ + --c_framework_dir "$(Build.BinariesDirectory)/build/framework_out" \ + --test_project_stage_dir "$(Build.BinariesDirectory)/app_center_test" \ + --prepare_test_project_only + displayName: "Assemble test project for App Center" + + # Xcode tasks require absolute paths because it searches for the paths and files relative to + # the root directory and not relative to the working directory + - task: Xcode@5 + inputs: + actions: 'build-for-testing' + configuration: 'Debug' + xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' + sdk: 'iphoneos' + scheme: 'ios_package_test' + signingOption: 'manual' + signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)' + provisioningProfileUuid: '$(APPLE_PROV_PROFILE_UUID)' + args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' + workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + useXcpretty: false # xcpretty can hide useful error output so we will disable it + displayName: 'Build App Center iPhone arm64 tests' + + - script: | + set -e -x + mkdir -p $(Build.ArtifactStagingDirectory)/package_test + zip -r --symlinks $(Build.ArtifactStagingDirectory)/package_test/package_tests.zip ios_package_testUITests-Runner.app + workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData/Build/Products/Debug-iphoneos' + displayName: "Create .zip file of the tests" + + - script: | + python $(Build.SourcesDirectory)/onnxruntime/test/platform/apple/generate_ipa_export_options_plist.py \ + --dest_file "exportOptions.plist" \ + --apple_team_id $(APPLE_TEAM_ID) \ + --provisioning_profile_uuid $(APPLE_PROV_PROFILE_UUID) + workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + displayName: "Generate .plist file for the .ipa file" + + # Task only generates an .xcarchive file if the plist export options are included, but does + # not produce an IPA file. + # Source code: https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/XcodeV5/xcode.ts + - task: Xcode@5 + inputs: + actions: 'archive' + xcWorkspacePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/apple_package_test.xcworkspace' + packageApp: true + archivePath: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + exportOptions: 'plist' + exportOptionsPlist: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/exportOptions.plist' + configuration: 'Debug' + sdk: 'iphoneos' + scheme: 'ios_package_test' + args: '-derivedDataPath $(Build.BinariesDirectory)/app_center_test/apple_package_test/DerivedData' + workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + useXcpretty: false + displayName: 'Create archive for the .ipa file' + + # Use script step because exporting the .ipa file using the Xcode@5 task was too brittle (Xcode@5 is designed + # to handle both the .xcarchive step and the .ipa step in the same step -- ran into countless issues with signing + # and the .plist file) + - script: | + xcodebuild -exportArchive \ + -archivePath ios_package_test.xcarchive \ + -exportOptionsPlist exportOptions.plist \ + -exportPath $(Build.ArtifactStagingDirectory)/package_test/test_ipa + workingDirectory: '$(Build.BinariesDirectory)/app_center_test/apple_package_test/' + displayName: "Create .ipa file" + + # Publish the BrowserStack artifacts first so that if the next step fails, the artifacts will still be published + # so that users can attempt to locally debug + - task: 1ES.PublishPipelineArtifact@1 + inputs: + path: $(Build.ArtifactStagingDirectory)/package_test + artifact: browserstack_test_artifacts_full + displayName: "Publish artifact - browserstack_test_artifacts_full" + + - script: | + set -e -x + pip install requests + python $(Build.SourcesDirectory)/tools/python/upload_and_run_browserstack_tests.py \ + --test_platform xcuitest \ + --app_path "$(Build.ArtifactStagingDirectory)/package_test/test_ipa/ios_package_test.ipa" \ + --test_path "$(Build.ArtifactStagingDirectory)/package_test/package_tests.zip" \ + --devices "iPhone 15-17" + displayName: Run E2E tests using Browserstack + workingDirectory: $(Build.BinariesDirectory)/app_center_test/apple_package_test + timeoutInMinutes: 15 + env: + BROWSERSTACK_ID: $(browserstack_username) + BROWSERSTACK_TOKEN: $(browserstack_access_key) + + - script: | + set -e -x + ls -R "$(Build.ArtifactStagingDirectory)" + displayName: "List staged artifacts" From 3a1e2e12741ba778e252574435b15ad4e690cde4 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:37:46 -0800 Subject: [PATCH 16/24] set BuildFramework jobs timeout to 90 min --- .../templates/stages/mac-ios-packaging-build-stage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 433d4df060908..2d10b94a408dc 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -72,6 +72,8 @@ stages: maxParallel: 8 matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] + timeoutInMinutes: 90 + steps: - template: ../setup-build-tools.yml parameters: From 25c18e73fc32c9bd4c9bfdac2dab042ff087afbb Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:24:52 -0800 Subject: [PATCH 17/24] increase BuildFramework job timeout to 120 min --- .../templates/stages/mac-ios-packaging-build-stage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 2d10b94a408dc..93969a32e9d1c 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -72,7 +72,7 @@ stages: maxParallel: 8 matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] - timeoutInMinutes: 90 + timeoutInMinutes: 120 steps: - template: ../setup-build-tools.yml From fdc363b4a6bb074598f1c854caa8f8a0d79b3c37 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:25:20 -0800 Subject: [PATCH 18/24] try macos-15 agents --- .../github/azure-pipelines/mac-ios-packaging-pipeline.yml | 4 +++- .../templates/stages/mac-ios-packaging-build-stage.yml | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml b/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml index 351cab9cfd171..4bcf4b05e77c0 100644 --- a/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/mac-ios-packaging-pipeline.yml @@ -56,7 +56,7 @@ extends: # Update the pool with your team's 1ES hosted pool. pool: name: "Azure Pipelines" - image: "macOS-14" + image: "macOS-15" os: macOS sdl: sourceAnalysisPool: @@ -67,3 +67,5 @@ extends: - template: templates/stages/mac-ios-packaging-build-stage.yml parameters: buildType: ${{ parameters.buildType }} + xcodeVersion: "16.4" + iosSimulatorRuntimeVersion: "18.5" diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 93969a32e9d1c..b11754eabd64f 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -4,7 +4,6 @@ parameters: values: - release - normal - default: normal # Note: Keep the Xcode version and iOS simulator version compatible. # Check the table here to see what iOS simulator versions are supported by a particular Xcode version: @@ -12,11 +11,9 @@ parameters: - name: xcodeVersion type: string - default: "16.2" - name: iosSimulatorRuntimeVersion type: string - default: "18.2" - name: buildSettingsFile type: string From 433473aacb2e451045225b249a12861d12431a69 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:46:45 -0800 Subject: [PATCH 19/24] add back source checkout --- .../templates/stages/mac-ios-packaging-build-stage.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index b11754eabd64f..aae6c6fd0c33e 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -125,8 +125,6 @@ stages: - job: AssemblePackageAndTest steps: - - checkout: none - - task: DownloadPipelineArtifact@2 inputs: itemPattern: framework_*/build.zip From 3831cdb81a445f66ae1f5ab9b6d0aaf1c1b6238f Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:19:27 -0800 Subject: [PATCH 20/24] DEBUG - use existing pipeline run for BuildFramework job artifacts --- .../stages/mac-ios-packaging-build-stage.yml | 188 +++++++++--------- 1 file changed, 98 insertions(+), 90 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index aae6c6fd0c33e..7fd8dc790f226 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -24,98 +24,100 @@ parameters: default: "onnxruntime" stages: -- stage: BuildFrameworks - dependsOn: [] - - jobs: - - job: SetUpBuildFrameworkJobMatrix - - steps: - - task: PythonScript@0 - name: SetVariables - inputs: - scriptSource: "inline" - script: | - import json - import sys - - utils_path = "$(Build.SourcesDirectory)/tools/ci_build/github/apple" - sys.path.insert(0, utils_path) - - from build_settings_utils import parse_build_settings_file, get_sysroot_arch_pairs - - build_settings_file = "${{ parameters.buildSettingsFile }}" - build_settings = parse_build_settings_file(build_settings_file) - sysroot_arch_pairs = get_sysroot_arch_pairs(build_settings) - - job_matrix = {} - for sysroot, arch in sysroot_arch_pairs: - identifier = f"{sysroot}_{arch}" - job_matrix[identifier] = { - "sysroot": sysroot, - "arch": arch, - } - - job_matrix_json = json.dumps(job_matrix) - - print(f"Build framework job matrix:\n{job_matrix_json}") - print(f"##vso[task.setvariable variable=BuildFrameworkJobMatrix;isOutput=true]{job_matrix_json}") - displayName: "Generate build framework job matrix" - - - job: BuildFramework - dependsOn: SetUpBuildFrameworkJobMatrix - - strategy: - maxParallel: 8 - matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] - - timeoutInMinutes: 120 - - steps: - - template: ../setup-build-tools.yml - parameters: - host_cpu_arch: arm64 - - - template: ../use-xcode-version.yml - parameters: - xcodeVersion: ${{ parameters.xcodeVersion }} - - - script: | - pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt - displayName: "Install Python requirements" - - - script: | - python tools/ci_build/github/apple/build_apple_framework.py \ - --build_dir "$(Build.BinariesDirectory)/build" \ - --only_build_single_sysroot_arch_framework "$(sysroot)" "$(arch)" \ - --record_sysroot_arch_framework_build_outputs_to_file "$(Build.BinariesDirectory)/build_outputs.txt" \ - "${{ parameters.buildSettingsFile }}" - displayName: "Build framework for $(sysroot)/$(arch)" - - - script: | - set -e - - BUILD_OUTPUTS_FILE="$(Build.BinariesDirectory)/build_outputs.txt" - BUILD_DIR="$(Build.BinariesDirectory)/build" - - pushd "${BUILD_DIR}" - zip \ - --recurse-paths \ - --symlinks \ - --names-stdin \ - "$(Build.ArtifactStagingDirectory)/build.zip" \ - < "${BUILD_OUTPUTS_FILE}" - popd - displayName: "Create framework build archive artifact" - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - path: $(Build.ArtifactStagingDirectory) - artifact: framework_$(sysroot)_$(arch) - displayName: "Publish artifact - framework_$(sysroot)_$(arch)" +# TODO commented out for testing +# - stage: BuildFrameworks +# dependsOn: [] + +# jobs: +# - job: SetUpBuildFrameworkJobMatrix + +# steps: +# - task: PythonScript@0 +# name: SetVariables +# inputs: +# scriptSource: "inline" +# script: | +# import json +# import sys + +# utils_path = "$(Build.SourcesDirectory)/tools/ci_build/github/apple" +# sys.path.insert(0, utils_path) + +# from build_settings_utils import parse_build_settings_file, get_sysroot_arch_pairs + +# build_settings_file = "${{ parameters.buildSettingsFile }}" +# build_settings = parse_build_settings_file(build_settings_file) +# sysroot_arch_pairs = get_sysroot_arch_pairs(build_settings) + +# job_matrix = {} +# for sysroot, arch in sysroot_arch_pairs: +# identifier = f"{sysroot}_{arch}" +# job_matrix[identifier] = { +# "sysroot": sysroot, +# "arch": arch, +# } + +# job_matrix_json = json.dumps(job_matrix) + +# print(f"Build framework job matrix:\n{job_matrix_json}") +# print(f"##vso[task.setvariable variable=BuildFrameworkJobMatrix;isOutput=true]{job_matrix_json}") +# displayName: "Generate build framework job matrix" + +# - job: BuildFramework +# dependsOn: SetUpBuildFrameworkJobMatrix + +# strategy: +# maxParallel: 8 +# matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] + +# timeoutInMinutes: 120 + +# steps: +# - template: ../setup-build-tools.yml +# parameters: +# host_cpu_arch: arm64 + +# - template: ../use-xcode-version.yml +# parameters: +# xcodeVersion: ${{ parameters.xcodeVersion }} + +# - script: | +# pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt +# displayName: "Install Python requirements" + +# - script: | +# python tools/ci_build/github/apple/build_apple_framework.py \ +# --build_dir "$(Build.BinariesDirectory)/build" \ +# --only_build_single_sysroot_arch_framework "$(sysroot)" "$(arch)" \ +# --record_sysroot_arch_framework_build_outputs_to_file "$(Build.BinariesDirectory)/build_outputs.txt" \ +# "${{ parameters.buildSettingsFile }}" +# displayName: "Build framework for $(sysroot)/$(arch)" + +# - script: | +# set -e + +# BUILD_OUTPUTS_FILE="$(Build.BinariesDirectory)/build_outputs.txt" +# BUILD_DIR="$(Build.BinariesDirectory)/build" + +# pushd "${BUILD_DIR}" +# zip \ +# --recurse-paths \ +# --symlinks \ +# --names-stdin \ +# "$(Build.ArtifactStagingDirectory)/build.zip" \ +# < "${BUILD_OUTPUTS_FILE}" +# popd +# displayName: "Create framework build archive artifact" + +# - task: 1ES.PublishPipelineArtifact@1 +# inputs: +# path: $(Build.ArtifactStagingDirectory) +# artifact: framework_$(sysroot)_$(arch) +# displayName: "Publish artifact - framework_$(sysroot)_$(arch)" - stage: AssemblePackageAndTest - dependsOn: BuildFrameworks + # TODO commented out for testing + # dependsOn: BuildFrameworks variables: cPodName: ${{ parameters.podNamePrefix }}-c @@ -129,6 +131,12 @@ stages: inputs: itemPattern: framework_*/build.zip targetPath: $(Build.BinariesDirectory)/artifacts + # TODO for testing + buildType: specific + project: Lotus + definition: "995" + runVersion: specific + pipelineId: "1078531" - script: | set -e From 02405304a3d52cdb9e83f2a47ef200595018cecb Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:33:02 -0800 Subject: [PATCH 21/24] reorder pipeline steps, clean up --- .../templates/setup-build-tools.yml | 23 +- .../stages/mac-ios-packaging-build-stage.yml | 277 +++++++++--------- 2 files changed, 142 insertions(+), 158 deletions(-) diff --git a/tools/ci_build/github/azure-pipelines/templates/setup-build-tools.yml b/tools/ci_build/github/azure-pipelines/templates/setup-build-tools.yml index 548ff8a54a854..f9441a9b74c4a 100644 --- a/tools/ci_build/github/azure-pipelines/templates/setup-build-tools.yml +++ b/tools/ci_build/github/azure-pipelines/templates/setup-build-tools.yml @@ -8,7 +8,7 @@ parameters: - name: python_version type: string default: '3.12' - + - name: action_version type: string default: 'v0.0.9' @@ -17,17 +17,9 @@ steps: - template: telemetry-steps.yml - task: UsePythonVersion@0 - displayName: 'Use Python ${{ parameters.host_cpu_arch }} (macOS)' - condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) - inputs: - versionSpec: ${{ parameters.python_version }} - architecture: ${{ parameters.host_cpu_arch }} - -- task: UsePythonVersion@0 - displayName: 'Use Python ${{ parameters.host_cpu_arch }} (non-macOS)' - condition: and(succeeded(), ne(variables['Agent.OS'], 'Darwin')) + displayName: 'Use Python ${{ parameters.python_version }} ${{ parameters.host_cpu_arch }}' inputs: - versionSpec: ${{ parameters.python_version }} + versionSpec: ${{ parameters.python_version }} architecture: ${{ parameters.host_cpu_arch }} - task: PipAuthenticate@1 @@ -45,17 +37,18 @@ steps: displayName: 'Setup Latest Node.js v20 (Win)' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) inputs: - filePath: '$(System.DefaultWorkingDirectory)\tools\ci_build\github\windows\setup_nodejs.ps1' + filePath: '$(System.DefaultWorkingDirectory)\tools\ci_build\github\windows\setup_nodejs.ps1' arguments: '-MajorVersion 22' - script: | node -v npm -v - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - displayName: 'Verify Node.js Version' + displayName: 'Display Node.js Version' -- script: python3 -m pip install requests +- script: | + python3 -m pip install requests + displayName: 'Install Python requests package' - task: PythonScript@0 displayName: 'Run GitHub Action via Python Wrapper' diff --git a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml index 7fd8dc790f226..16b7489241e5a 100644 --- a/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/stages/mac-ios-packaging-build-stage.yml @@ -5,9 +5,9 @@ parameters: - release - normal - # Note: Keep the Xcode version and iOS simulator version compatible. - # Check the table here to see what iOS simulator versions are supported by a particular Xcode version: - # https://developer.apple.com/support/xcode/ +# Note: Keep the Xcode version and iOS simulator version compatible. +# Check the table here to see what iOS simulator versions are supported by a particular Xcode version: +# https://developer.apple.com/support/xcode/ - name: xcodeVersion type: string @@ -24,130 +24,110 @@ parameters: default: "onnxruntime" stages: -# TODO commented out for testing -# - stage: BuildFrameworks -# dependsOn: [] - -# jobs: -# - job: SetUpBuildFrameworkJobMatrix - -# steps: -# - task: PythonScript@0 -# name: SetVariables -# inputs: -# scriptSource: "inline" -# script: | -# import json -# import sys - -# utils_path = "$(Build.SourcesDirectory)/tools/ci_build/github/apple" -# sys.path.insert(0, utils_path) - -# from build_settings_utils import parse_build_settings_file, get_sysroot_arch_pairs - -# build_settings_file = "${{ parameters.buildSettingsFile }}" -# build_settings = parse_build_settings_file(build_settings_file) -# sysroot_arch_pairs = get_sysroot_arch_pairs(build_settings) - -# job_matrix = {} -# for sysroot, arch in sysroot_arch_pairs: -# identifier = f"{sysroot}_{arch}" -# job_matrix[identifier] = { -# "sysroot": sysroot, -# "arch": arch, -# } - -# job_matrix_json = json.dumps(job_matrix) - -# print(f"Build framework job matrix:\n{job_matrix_json}") -# print(f"##vso[task.setvariable variable=BuildFrameworkJobMatrix;isOutput=true]{job_matrix_json}") -# displayName: "Generate build framework job matrix" - -# - job: BuildFramework -# dependsOn: SetUpBuildFrameworkJobMatrix - -# strategy: -# maxParallel: 8 -# matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] - -# timeoutInMinutes: 120 - -# steps: -# - template: ../setup-build-tools.yml -# parameters: -# host_cpu_arch: arm64 - -# - template: ../use-xcode-version.yml -# parameters: -# xcodeVersion: ${{ parameters.xcodeVersion }} - -# - script: | -# pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt -# displayName: "Install Python requirements" - -# - script: | -# python tools/ci_build/github/apple/build_apple_framework.py \ -# --build_dir "$(Build.BinariesDirectory)/build" \ -# --only_build_single_sysroot_arch_framework "$(sysroot)" "$(arch)" \ -# --record_sysroot_arch_framework_build_outputs_to_file "$(Build.BinariesDirectory)/build_outputs.txt" \ -# "${{ parameters.buildSettingsFile }}" -# displayName: "Build framework for $(sysroot)/$(arch)" - -# - script: | -# set -e - -# BUILD_OUTPUTS_FILE="$(Build.BinariesDirectory)/build_outputs.txt" -# BUILD_DIR="$(Build.BinariesDirectory)/build" - -# pushd "${BUILD_DIR}" -# zip \ -# --recurse-paths \ -# --symlinks \ -# --names-stdin \ -# "$(Build.ArtifactStagingDirectory)/build.zip" \ -# < "${BUILD_OUTPUTS_FILE}" -# popd -# displayName: "Create framework build archive artifact" - -# - task: 1ES.PublishPipelineArtifact@1 -# inputs: -# path: $(Build.ArtifactStagingDirectory) -# artifact: framework_$(sysroot)_$(arch) -# displayName: "Publish artifact - framework_$(sysroot)_$(arch)" - -- stage: AssemblePackageAndTest - # TODO commented out for testing - # dependsOn: BuildFrameworks - - variables: - cPodName: ${{ parameters.podNamePrefix }}-c - objcPodName: ${{ parameters.podNamePrefix }}-objc +# This stage builds the individual sysroot/arch frameworks. +# Since the number of framework build jobs is dynamic, these jobs are put into this stage to allow a later stage to +# wait for all of them to complete. +- stage: BuildFrameworks + dependsOn: [] jobs: - - job: AssemblePackageAndTest + - job: SetUpBuildFrameworkJobMatrix steps: - - task: DownloadPipelineArtifact@2 + - task: PythonScript@0 + name: SetVariables inputs: - itemPattern: framework_*/build.zip - targetPath: $(Build.BinariesDirectory)/artifacts - # TODO for testing - buildType: specific - project: Lotus - definition: "995" - runVersion: specific - pipelineId: "1078531" + scriptSource: "inline" + script: | + import json + import sys + + utils_path = "$(Build.SourcesDirectory)/tools/ci_build/github/apple" + sys.path.insert(0, utils_path) + + from build_settings_utils import parse_build_settings_file, get_sysroot_arch_pairs + + build_settings_file = "${{ parameters.buildSettingsFile }}" + build_settings = parse_build_settings_file(build_settings_file) + sysroot_arch_pairs = get_sysroot_arch_pairs(build_settings) + + job_matrix = {} + for sysroot, arch in sysroot_arch_pairs: + identifier = f"{sysroot}_{arch}" + job_matrix[identifier] = { + "sysroot": sysroot, + "arch": arch, + } + + job_matrix_json = json.dumps(job_matrix) + + print(f"Build framework job matrix:\n{job_matrix_json}") + print(f"##vso[task.setvariable variable=BuildFrameworkJobMatrix;isOutput=true]{job_matrix_json}") + displayName: "Generate build framework job matrix" + + - job: BuildFramework + dependsOn: SetUpBuildFrameworkJobMatrix + + strategy: + maxParallel: 8 + matrix: $[ dependencies.SetUpBuildFrameworkJobMatrix.outputs['SetVariables.BuildFrameworkJobMatrix'] ] + + timeoutInMinutes: 120 + + steps: + - template: ../setup-build-tools.yml + parameters: + host_cpu_arch: arm64 + + - template: ../use-xcode-version.yml + parameters: + xcodeVersion: ${{ parameters.xcodeVersion }} + + - script: | + pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt + displayName: "Install Python requirements" + + - script: | + python tools/ci_build/github/apple/build_apple_framework.py \ + --build_dir "$(Build.BinariesDirectory)/build" \ + --only_build_single_sysroot_arch_framework "$(sysroot)" "$(arch)" \ + --record_sysroot_arch_framework_build_outputs_to_file "$(Build.BinariesDirectory)/build_outputs.txt" \ + "${{ parameters.buildSettingsFile }}" + displayName: "Build framework for $(sysroot)/$(arch)" - script: | set -e + BUILD_OUTPUTS_FILE="$(Build.BinariesDirectory)/build_outputs.txt" BUILD_DIR="$(Build.BinariesDirectory)/build" - mkdir -p "${BUILD_DIR}" - find "$(Build.BinariesDirectory)/artifacts" -name "build.zip" -exec unzip {} -d "${BUILD_DIR}" \; - find "${BUILD_DIR}" - displayName: Extract framework build archive artifacts to build directory + cd "${BUILD_DIR}" + zip \ + --recurse-paths \ + --symlinks \ + --names-stdin \ + "$(Build.ArtifactStagingDirectory)/build.zip" \ + < "${BUILD_OUTPUTS_FILE}" + displayName: "Create framework build archive artifact" + + - task: 1ES.PublishPipelineArtifact@1 + inputs: + path: $(Build.ArtifactStagingDirectory) + artifact: framework_$(sysroot)_$(arch) + displayName: "Publish artifact - framework_$(sysroot)_$(arch)" +# This stage assembles the frameworks built in the previous stage into a packaging artifacts and tests them. +- stage: AssemblePackageAndTest + dependsOn: BuildFrameworks + + variables: + cPodName: ${{ parameters.podNamePrefix }}-c + objcPodName: ${{ parameters.podNamePrefix }}-objc + + jobs: + - job: AssemblePackageAndTest + + steps: - script: | set -e @@ -168,7 +148,7 @@ stages: # Do not output ##vso[] commands with `set -x` or they may be parsed again and include a trailing quote. set +x echo "##vso[task.setvariable variable=ortPodVersion;]${VERSION}" - echo "ortPodVersion : ${ortPodVersion}, VERSION : ${VERSION}" + echo "ortPodVersion: ${VERSION}" displayName: "Set ortPodVersion variable" - task: InstallAppleCertificate@2 @@ -197,6 +177,19 @@ stages: pip install -r tools/ci_build/github/apple/ios_packaging/requirements.txt displayName: "Install Python requirements" + - task: DownloadPipelineArtifact@2 + inputs: + itemPattern: framework_*/build.zip + targetPath: $(Build.BinariesDirectory)/artifacts + displayName: "Download framework build archive artifacts" + + - script: | + set -e + BUILD_DIR="$(Build.BinariesDirectory)/build" + mkdir -p "${BUILD_DIR}" + find "$(Build.BinariesDirectory)/artifacts" -name "build.zip" -exec unzip {} -d "${BUILD_DIR}" \; + displayName: "Extract framework build archive artifacts to build directory" + # Assemble the xcframework and the pod packages. # The frameworks from the BuildFrameworks stage should already be in the build directory. - script: | @@ -211,33 +204,6 @@ stages: env: ORT_GET_SIMULATOR_DEVICE_INFO_REQUESTED_RUNTIME_VERSION: ${{ parameters.iosSimulatorRuntimeVersion }} - - script: | - set -e -x - - mkdir -p "$(Build.ArtifactStagingDirectory)/package" - - for POD_NAME in "${{ variables.cPodName}}" "${{ variables.objcPodName }}"; - do - ./tools/ci_build/github/apple/assemble_apple_packaging_artifacts.sh \ - "$(Build.BinariesDirectory)/staging" \ - "$(Build.ArtifactStagingDirectory)/package" \ - "${POD_NAME}" \ - "$(ortPodVersion)" - done - - # copy over helper script for use in release pipeline - cp tools/ci_build/github/apple/package_release_tasks.py "$(Build.ArtifactStagingDirectory)/package" - displayName: "Assemble packaging artifacts" - - # Publish package artifact - - task: 1ES.PublishPipelineArtifact@1 - inputs: - path: $(Build.ArtifactStagingDirectory)/package - artifact: ios_packaging_artifacts_full - displayName: "Publish artifact - ios_packaging_artifacts_full" - - # Run more tests on the package - - script: | python tools/ci_build/github/apple/test_apple_packages.py \ --fail_if_cocoapods_missing \ @@ -332,6 +298,31 @@ stages: BROWSERSTACK_ID: $(browserstack_username) BROWSERSTACK_TOKEN: $(browserstack_access_key) + - script: | + set -e -x + + mkdir -p "$(Build.ArtifactStagingDirectory)/package" + + for POD_NAME in "${{ variables.cPodName}}" "${{ variables.objcPodName }}"; + do + ./tools/ci_build/github/apple/assemble_apple_packaging_artifacts.sh \ + "$(Build.BinariesDirectory)/staging" \ + "$(Build.ArtifactStagingDirectory)/package" \ + "${POD_NAME}" \ + "$(ortPodVersion)" + done + + # copy over helper script for use in release pipeline + cp tools/ci_build/github/apple/package_release_tasks.py "$(Build.ArtifactStagingDirectory)/package" + displayName: "Assemble packaging artifacts" + + # Publish packaging artifacts + - task: 1ES.PublishPipelineArtifact@1 + inputs: + path: $(Build.ArtifactStagingDirectory)/package + artifact: ios_packaging_artifacts_full + displayName: "Publish artifact - ios_packaging_artifacts_full" + - script: | set -e -x ls -R "$(Build.ArtifactStagingDirectory)" From 6d79be8a0814fae91c2208c7ef31328c028f35ef Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:31:52 -0800 Subject: [PATCH 22/24] lint issues --- .../ci_build/github/apple/build_settings_utils.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tools/ci_build/github/apple/build_settings_utils.py b/tools/ci_build/github/apple/build_settings_utils.py index b17cbd7368e2e..07fda38b6f525 100644 --- a/tools/ci_build/github/apple/build_settings_utils.py +++ b/tools/ci_build/github/apple/build_settings_utils.py @@ -8,15 +8,16 @@ "iphonesimulator": ["arm64", "x86_64"], } + def parse_build_settings_file(build_settings_file: pathlib.Path) -> dict: - ''' + """ Parses the provided build settings file into a build settings dict. :param build_settings_file: The build settings file path. :type build_settings_file: pathlib.Path :return: The build settings dict. :rtype: dict[Any, Any] - ''' + """ with open(build_settings_file) as f: build_settings_data = json.load(f) @@ -43,24 +44,24 @@ def validate_str_to_str_list_dict(input: dict[str, list[str]]): def get_sysroot_arch_pairs(build_settings: dict) -> list[tuple[str, str]]: - ''' + """ Gets all specified sysroot/arch pairs. :param build_settings: The build settings dict. :type build_settings: dict :return: A list of (sysroot, arch) tuples. :rtype: list[tuple[str, str]] - ''' + """ pair_set: set[tuple[str, str]] = set() for sysroot, archs in build_settings["build_osx_archs"].items(): for arch in archs: pair_set.add((sysroot, arch)) - return sorted(list(pair_set)) + return sorted(pair_set) def get_build_params(build_settings: dict, sysroot: str) -> list[str]: - ''' + """ Returns the build params associated with given `sysroot`. The special `sysroot` value "base" may be used to get the base build params. @@ -70,5 +71,5 @@ def get_build_params(build_settings: dict, sysroot: str) -> list[str]: :type sysroot: str :return: The build params associated with `sysroot`, if any, or an empty list. :rtype: list[str] - ''' + """ return build_settings["build_params"].get(sysroot, []) From 47ba54cd37f9b84a745e1362549cb6a52e6607cd Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:52:30 -0800 Subject: [PATCH 23/24] add dict type annotation --- tools/ci_build/github/apple/build_settings_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/ci_build/github/apple/build_settings_utils.py b/tools/ci_build/github/apple/build_settings_utils.py index 07fda38b6f525..e9c8b76cb0384 100644 --- a/tools/ci_build/github/apple/build_settings_utils.py +++ b/tools/ci_build/github/apple/build_settings_utils.py @@ -2,6 +2,7 @@ import json import pathlib +import typing _DEFAULT_BUILD_SYSROOT_ARCHS = { "iphoneos": ["arm64"], @@ -9,14 +10,14 @@ } -def parse_build_settings_file(build_settings_file: pathlib.Path) -> dict: +def parse_build_settings_file(build_settings_file: pathlib.Path) -> dict[str, typing.Any]: """ Parses the provided build settings file into a build settings dict. :param build_settings_file: The build settings file path. :type build_settings_file: pathlib.Path :return: The build settings dict. - :rtype: dict[Any, Any] + :rtype: dict[str, Any] """ with open(build_settings_file) as f: build_settings_data = json.load(f) From 7cfc108acd02cf1cdbc150c6667c2736ec4c699d Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:51:07 -0800 Subject: [PATCH 24/24] replace assert with check helper function --- .../github/apple/build_settings_utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tools/ci_build/github/apple/build_settings_utils.py b/tools/ci_build/github/apple/build_settings_utils.py index e9c8b76cb0384..cd0c76c5a0ecc 100644 --- a/tools/ci_build/github/apple/build_settings_utils.py +++ b/tools/ci_build/github/apple/build_settings_utils.py @@ -19,17 +19,22 @@ def parse_build_settings_file(build_settings_file: pathlib.Path) -> dict[str, ty :return: The build settings dict. :rtype: dict[str, Any] """ - with open(build_settings_file) as f: - build_settings_data = json.load(f) + + def check(condition: bool, message: str): + if not condition: + raise ValueError(message) # validate that `input` is a dict[str, list[str]] def validate_str_to_str_list_dict(input: dict[str, list[str]]): - assert isinstance(input, dict), f"input is not a dict: {input}" + check(isinstance(input, dict), f"input is not a dict: {input}") for key, value in input.items(): - assert isinstance(key, str), f"key is not a string: {key}" - assert isinstance(value, list), f"value is not a list: {value}" + check(isinstance(key, str), f"key is not a string: {key}") + check(isinstance(value, list), f"value is not a list: {value}") for value_element in value: - assert isinstance(value_element, str), f"list element is not a string: {value_element}" + check(isinstance(value_element, str), f"list element is not a string: {value_element}") + + with open(build_settings_file) as f: + build_settings_data = json.load(f) build_settings = {}