# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # pylint: disable=invalid-name # ruff: noqa: E501, RUF005 """Utility to invoke Xcode compiler toolchain""" import json import os import subprocess import sys from . import utils def xcrun(cmd): """Run xcrun and return the output. Parameters ---------- cmd : list of str The command sequence. Returns ------- out : str The output string. """ cmd = ["xcrun"] + cmd proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) (out, _) = proc.communicate() return out.strip() def __get_min_os_version(sdk): if sdk == "macosx": return None if sdk in ("iphoneos", "iphonesimulator"): return "13.0" raise RuntimeError(f"Unsupported sdk: {sdk}") def __get_min_os_version_cmd(sdk, min_os_version): if min_os_version is None: min_os_version = __get_min_os_version(sdk) if min_os_version is not None: return "-mios-version-min=" + min_os_version return "" def create_dylib(output, objects, arch, sdk="macosx", min_os_version=None): """Create dynamic library. Parameters ---------- output : str The target shared library. objects : list List of object files. options : str The additional options. arch : str Target major architectures sdk : str The sdk to be used. """ clang = xcrun(["-sdk", sdk, "-find", "clang"]) sdk_path = xcrun(["-sdk", sdk, "--show-sdk-path"]) cmd = [clang] cmd += ["-dynamiclib"] cmd += ["-arch", arch] cmd += ["-isysroot", sdk_path] cmd += [__get_min_os_version_cmd(sdk, min_os_version)] cmd += ["-o", output] if isinstance(objects, str): cmd += [objects] else: cmd += objects proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) (out, _) = proc.communicate() if proc.returncode != 0: msg = "Compilation error:\n" msg += out.decode("utf-8", errors="replace") raise RuntimeError(msg) # assign so as default output format create_dylib.output_format = "dylib" def compile_metal(code, path_target=None, sdk="macosx", min_os_version=None): """Compile Metal with CLI tool from env. Parameters ---------- code : str The Metal code. path_target : str, optional Output file. sdk : str, optional The target platform SDK. Return ------ metallib : bytearray The bytearray of the metallib """ temp = utils.tempdir() temp_code = temp.relpath("my_lib.metal") temp_ir = temp.relpath("my_lib.air") temp_target = temp.relpath("my_lib.metallib") with open(temp_code, "w") as out_file: out_file.write(code) file_target = path_target if path_target else temp_target # See: # - https://developer.apple.com/documentation/metal/gpu_functions_libraries/building_a_library_with_metal_s_command-line_tools#overview # pylint: disable=line-too-long # # xcrun -sdk macosx metal -c MyLibrary.metal -o MyLibrary.air # xcrun -sdk macosx metallib MyLibrary.air -o MyLibrary.metallib min_target = __get_min_os_version_cmd(sdk, min_os_version) if sdk == "macosx": language_version = "-std=macos-metal2.3" elif sdk in ("iphoneos", "iphonesimulator"): language_version = "-std=ios-metal2.3" else: raise RuntimeError(f"Unsupported sdk: {sdk}") cmd1 = ["xcrun", "-sdk", sdk, "metal", language_version, min_target, "-O3"] cmd1 += ["-c", temp_code, "-o", temp_ir] cmd2 = ["xcrun", "-sdk", sdk, "metallib"] cmd2 += [temp_ir, "-o", file_target] proc = subprocess.Popen( " ".join(cmd1) + ";" + " ".join(cmd2), shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) (out, _) = proc.communicate() if proc.returncode != 0: sys.stderr.write("Compilation error:\n") sys.stderr.write(out.decode("utf-8", errors="replace")) sys.stderr.flush() libbin = None else: libbin = bytearray(open(file_target, "rb").read()) return libbin def compile_coreml(model, model_name="main", out_dir="."): """Compile coreml model and return the compiled model path.""" mlmodel_path = os.path.join(out_dir, model_name + ".mlmodel") mlmodelc_path = os.path.join(out_dir, model_name + ".mlmodelc") metadata = {"inputs": list(model.input_description), "outputs": list(model.output_description)} # Use the description field to send info to CoreML runtime model.short_description = json.dumps(metadata) model.save(mlmodel_path) res = xcrun(["coremlcompiler", "compile", mlmodel_path, out_dir]) if not os.path.isdir(mlmodelc_path): raise RuntimeError(f"Compile failed: {res}") return mlmodelc_path