"""Cangjie ObjectiveC interoperability build script"""
import argparse
import fileinput
import logging
import os
import platform
import re
import shutil
import subprocess
import sys
import tarfile
from enum import Enum
from logging.handlers import TimedRotatingFileHandler
from subprocess import PIPE
IS_DARWIN = platform.system() == "Darwin"
BUILD_DIR = os.path.dirname(os.path.abspath(__file__))
HOME_DIR = os.path.dirname(BUILD_DIR)
OBJC_INTEROP_GEN_DIR = os.path.join(HOME_DIR, 'src', 'ObjCInteropGen')
CMAKE_BUILD_DIR = os.path.join(OBJC_INTEROP_GEN_DIR, 'build', 'build')
DEFAULT_INSTALL_DIR = os.path.join(HOME_DIR, 'dist')
INTEROPLIB_DIR = os.path.join(HOME_DIR, 'src', 'interoplib')
INTEROPLIB_OUT = os.path.join(INTEROPLIB_DIR, 'output')
DYLIB_EXT = "dylib" if IS_DARWIN else "so"
INTEROPLIB_OBJCLIB_DIR = os.path.join(INTEROPLIB_DIR, 'src', 'objclib')
RELEASE = "release"
INTEROPLIB_NAME_IN_TOML = "interoplib"
OBJC_NAME_IN_TOML = "objc"
INTEROPLIB_OUT_PREFIX = os.path.join(INTEROPLIB_OUT, RELEASE, INTEROPLIB_NAME_IN_TOML)
OBJC_OUT_PREFIX = os.path.join(INTEROPLIB_OUT, RELEASE, OBJC_NAME_IN_TOML)
OUT_OBJC_INTERNAL_DYLIB = os.path.join(OBJC_OUT_PREFIX, f"libobjc.internal.{DYLIB_EXT}")
OUT_OBJC_INTERNAL_A = os.path.join(OBJC_OUT_PREFIX, "libobjc.internal.a")
OUT_OBJC_INTERNAL_CJO = os.path.join(OBJC_OUT_PREFIX, "objc.internal.cjo")
OUT_OBJC_LANG_DYLIB = os.path.join(OBJC_OUT_PREFIX, f"libobjc.lang.{DYLIB_EXT}")
OUT_OBJC_LANG_A = os.path.join(OBJC_OUT_PREFIX, "libobjc.lang.a")
OUT_OBJC_LANG_CJO = os.path.join(OBJC_OUT_PREFIX, "objc.lang.cjo")
OUT_INTEROPLIB_OBJCLIB_DYLIB = os.path.join(INTEROPLIB_OUT_PREFIX, f"libinteroplib.objclib.{DYLIB_EXT}")
OUT_INTEROPLIB_OBJCLIB_A = os.path.join(INTEROPLIB_OUT_PREFIX, "libinteroplib.objclib.a")
LOG_DIR = os.path.join(BUILD_DIR, 'logs')
LOG_FILE = os.path.join(LOG_DIR, 'ObjCInteropGen.log')
def log_output(output):
"""log command output"""
while True:
line = output.stdout.readline()
if not line:
output.communicate()
returncode = output.returncode
if returncode != 0:
LOG.critical('build error: %d!\n', returncode)
sys.exit(1)
break
try:
LOG.info(line.decode('ascii', 'ignore').rstrip())
except UnicodeEncodeError:
LOG.info(line.decode('utf-8', 'ignore').rstrip())
def init_log(name):
"""init log config"""
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
log = logging.getLogger(name)
log.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'[%(asctime)s:%(module)s:%(lineno)s:%(levelname)s] %(message)s')
streamhandler = logging.StreamHandler(sys.stdout)
streamhandler.setLevel(logging.DEBUG)
streamhandler.setFormatter(formatter)
log.addHandler(streamhandler)
filehandler = TimedRotatingFileHandler(LOG_FILE,
when='W6',
interval=1,
backupCount=60)
filehandler.setLevel(logging.DEBUG)
filehandler.setFormatter(formatter)
log.addHandler(filehandler)
return log
def fatal(message):
"""Log the message as CRITICAL and raise Exception with the same message"""
LOG.critical(message)
raise Exception(message)
def fixedEnv(env=None):
if env is None:
env = os.environ.copy()
env["ZERO_AR_DATE"] = "1"
return env
def check_clang(args):
toolchain_path = args.target_toolchain if args.target_toolchain else None
if toolchain_path and (not os.path.exists(toolchain_path)):
LOG.error(f"The given toolchain path does not exist: {toolchain_path}")
c_compiler = shutil.which("clang", path=toolchain_path)
if c_compiler is None:
if toolchain_path:
LOG.error(f"Cannot find clang in the given toolchain path: {toolchain_path}")
else:
LOG.error("Cannot find clang in $PATH")
fatal("clang is required to build interop libraries")
return c_compiler
def command(*args, cwd=None, env=None):
"""Execute a child program via 'subprocess.Popen' and log the output"""
output = subprocess.Popen(args, stdout=PIPE, cwd=cwd, env=fixedEnv(env))
log_output(output)
if output.returncode:
fatal('"' + ' '.join(args) + '" returned ' + output.returncode)
def adjust_target(target):
match target:
case "ios-aarch64": return "arm64-apple-ios11"
case "ios-simulator-aarch64": return "arm64-apple-ios11-simulator"
case "ios-simulator-x86_64": return "x86_64-apple-ios11-simulator"
case _: return target
def runtime_name(target):
return target+"_cjnative"
def download_and_patch_tinytoml():
"""Set up the tinytoml third-party library"""
TINYTOML_DIR = os.path.join(HOME_DIR, "third_party", "tinytoml")
PATCH_FILE = os.path.join(TINYTOML_DIR, "fixFloatEqualError.patch")
TAR_PATH = os.path.join(TINYTOML_DIR, "tinytoml-0.4.tar.gz")
TINYTOMLTAR_PATH = os.path.join(TINYTOML_DIR, "tinytoml-0.4")
LOG.info("Setting up tinytoml...")
os.makedirs(os.path.dirname(TINYTOML_DIR), exist_ok=True)
try:
if not os.path.exists(TAR_PATH):
subprocess.run(
["git", "clone", "https://gitcode.com/src-openeuler/tinytoml.git",
TINYTOML_DIR],
check=True
)
subprocess.run(
["git", "checkout", "openEuler-24.03-LTS-SP1"],
cwd=TINYTOML_DIR,
check=True
)
LOG.info("Extracting tinytoml package...")
with tarfile.open(TAR_PATH) as tar:
tar.extractall(path=TINYTOML_DIR)
patch_file_exists = os.path.exists(PATCH_FILE)
target_dir_exists = os.path.exists(TINYTOMLTAR_PATH) and os.path.isdir(TINYTOMLTAR_PATH)
if patch_file_exists and target_dir_exists:
LOG.info("Applying tinytoml patch...")
subprocess.run(
f"patch -p0 -l -f < {PATCH_FILE}",
cwd=TINYTOMLTAR_PATH,
shell=True,
check=True
)
else:
LOG.warning(f"Patch file not found: {PATCH_FILE}")
TOML_H_SRC = os.path.join(TINYTOMLTAR_PATH, "include", "toml", "toml.h")
TOML_H_DST = os.path.join(TINYTOML_DIR, "toml.h")
if os.path.exists(TOML_H_SRC):
LOG.info(f"Copying toml.h from {TOML_H_SRC} to {TOML_H_DST}")
shutil.copy2(TOML_H_SRC, TOML_H_DST)
except subprocess.CalledProcessError as e:
LOG.error(f"Failed to setup tinytoml: {str(e)}")
if os.path.exists(TINYTOML_DIR):
shutil.rmtree(TINYTOML_DIR)
raise
def replace_in_file(text_to_search, replacement_text, filename):
with fileinput.FileInput(filename, inplace=True) as file:
for line in file:
print(line.replace(text_to_search, replacement_text), end='')
def build(args):
"""interoplib or objc-interop-gen build"""
if args.target:
runtime = runtime_name(args.target)
LOG.info('begin build interoplib for ' + runtime + '\n')
CJPM_CONFIG = "--cfg_darwin_objc" if IS_DARWIN else "--cfg_linux_objc"
cjpm_env = os.environ.copy()
if args.target_lib:
cjpm_target = adjust_target(args.target_lib)
cjpm_env["TARGET_OPTION"] = "--target=" + cjpm_target
if args.target_sysroot:
cjpm_env["SYSROOT_OPTION"] = "--sysroot=" + args.target_sysroot
TOML = os.path.join(INTEROPLIB_DIR, 'objc', 'cjpm.toml')
replace_in_file('output-type = "dynamic"', 'output-type = "static"', TOML)
LOG.info('build interoplib into static libs:\n')
command("cjpm", "build", "--target-dir=" + INTEROPLIB_OUT, CJPM_CONFIG, cwd=INTEROPLIB_DIR, env=cjpm_env)
replace_in_file('output-type = "static"', 'output-type = "dynamic"', TOML)
LOG.info('build interoplib into dynamic libs:\n')
command("cjpm", "build", "--target-dir=" + INTEROPLIB_OUT, CJPM_CONFIG, cwd=INTEROPLIB_DIR, env=cjpm_env)
clang_compiler = check_clang(args)
clang_opts = []
if args.target_lib:
clang_target = adjust_target(args.target_lib)
clang_opts += [f"--target={clang_target}"]
if args.target_sysroot:
clang_opts += [f"-isysroot{args.target_sysroot}"]
if not IS_DARWIN:
clang_opts += subprocess.run(['gnustep-config', '--objc-flags'], capture_output=True).stdout.decode().split()
OBJCLIB_O = os.path.join(prepare_dir(INTEROPLIB_OUT_PREFIX), "objclib.o")
clang_command_o = [clang_compiler, "-fmodules", "-c", "-fPIC"] + clang_opts.copy() + ["-I.", "cjinterop.m", f"-o{OBJCLIB_O}"]
command(*clang_command_o.copy(), cwd=INTEROPLIB_OBJCLIB_DIR)
command(
"ar", "-cr", OUT_INTEROPLIB_OBJCLIB_A, OBJCLIB_O,
cwd=INTEROPLIB_OBJCLIB_DIR,
)
command(
"ranlib", "-D", OUT_INTEROPLIB_OBJCLIB_A,
cwd=INTEROPLIB_OBJCLIB_DIR,
)
clang_command_so = [clang_compiler, "-shared"] + clang_opts.copy()
if IS_DARWIN:
clang_command_so += ["-lobjc"]
else:
clang_command_so += subprocess.run(['gnustep-config', '--objc-libs'], capture_output=True).stdout.decode().split()
clang_command_so += [f"-L{os.environ['CANGJIE_HOME']}/runtime/lib/{runtime}", "-lcangjie-runtime"]
clang_command_so += [f"-o{OUT_INTEROPLIB_OBJCLIB_DYLIB}", OBJCLIB_O]
command(*clang_command_so.copy(), cwd=INTEROPLIB_OBJCLIB_DIR)
LOG.info('end build interoplib for ' + runtime + '\n')
else:
LOG.info('begin build objc-interop-gen...\n')
if args.toml_dir:
TOML_DIR = args.toml_dir
else:
TOML_DIR = os.path.join(HOME_DIR, "third_party", "tinytoml");
tinytoml_target = os.path.join(TOML_DIR, "toml.h")
if not os.path.exists(tinytoml_target):
download_and_patch_tinytoml()
cmake_args = ["-B", CMAKE_BUILD_DIR, "-DCMAKE_BUILD_TYPE=" + args.build_type.value, "-DTOML_DIR=" + TOML_DIR]
if args.llvm_src_dir or args.llvm_build_dir:
if not args.llvm_src_dir or not args.llvm_build_dir:
LOG.critical("Both directories must be specified: --llvm-src-dir and --llvm-build-dir")
sys.exit(1)
cmake_args.append("-DLLVM_SRC_DIR=" + args.llvm_src_dir)
cmake_args.append("-DLLVM_BUILD_DIR=" + args.llvm_build_dir)
command("cmake", *cmake_args, cwd=OBJC_INTEROP_GEN_DIR)
command("cmake", "--build", CMAKE_BUILD_DIR, cwd=OBJC_INTEROP_GEN_DIR)
LOG.info('end build objc-interop-gen\n')
def clean(args):
"""clean build outputs and logs"""
LOG.info("begin clean objc-interop-gen...\n")
if os.path.isdir(CMAKE_BUILD_DIR):
shutil.rmtree(CMAKE_BUILD_DIR, ignore_errors=True)
LOG.info("end clean objc-interop-gen\n")
LOG.info("begin clean interoplib...\n")
command("cjpm", "clean", "--target-dir=" + INTEROPLIB_OUT, cwd=INTEROPLIB_DIR)
LOG.info("end clean interoplib\n")
def prepare_dir(base_path, *relative_path):
"""
Ensure that the directory specified by the arguments exists (create it if it
does not) and return its path.
"""
path = os.path.join(base_path, *relative_path)
if not os.path.exists(path):
os.makedirs(path)
return path
def install_file(install_dir, file):
if os.path.isfile(file):
shutil.copy2(file, install_dir)
else:
fatal("Cannot find \"" + file + "\" for installing to \"" + install_dir + "\"")
def install_files(install_dir, *files):
for file in files:
install_file(install_dir, file)
def find_match(lines, regexp):
"""Searching the list of lines, find the first substring that matches the capture in the specified pattern"""
pattern = re.compile(regexp)
for line in lines:
matched = re.search(pattern, line)
if matched:
return matched.group(1)
return None
def change_dependency_install_name(binary, otool_output, library):
"""
Change the dependent library install name to the @rpath-based one. If the
binary does not contain an install name for this library, raise an exception.
Arguments:
binary - the name of the binary file where to change the dependency
otool_output - the output of the `otool -l <binary>` command, splitted into
lines
library - the file name (without a path) of the library
"""
path = find_match(otool_output, r"name (.*/" + library + r") \(offset \d*\)")
if path:
command("install_name_tool", "-change", path, "@rpath/" + library, binary)
else:
fatal("Expected dependency on \"" + library + "\" in \"" + binary + "\" not found")
def change_install_names(dylib, dependencies):
"""
Set @rpath in the specified dynamic library and change the install names of its
dependencies to @rpath-based ones. If the binary does not contain any of the
specified dependencies, raise an exception.
Arguments:
dylib - the name of the dynamic library where to change the dependency
dependencies - array of dependency file names (without paths)
"""
otool_output = subprocess.run(["otool", "-l", dylib], capture_output=True, env=fixedEnv()).stdout.decode().splitlines()
rpath = find_match(otool_output, r"path (.*) \(offset \d*\)")
command("install_name_tool", "-id", "@rpath/" + os.path.basename(dylib), dylib)
if rpath:
command("install_name_tool", "-rpath", rpath, "@loader_path", dylib)
else:
command("install_name_tool", "-add_rpath", "@loader_path", dylib)
for dependency in dependencies:
change_dependency_install_name(dylib, otool_output, dependency)
def install(args):
"""install objc-interop-gen or interoplib"""
install_path = os.path.abspath(args.install_prefix) if args.install_prefix else DEFAULT_INSTALL_DIR
if args.target:
runtime = runtime_name(args.target)
LOG.info("begin install interoplib for " + runtime + "\n")
installation_dir_static = prepare_dir(install_path, "lib", runtime)
install_files(
installation_dir_static,
OUT_OBJC_INTERNAL_A,
OUT_OBJC_LANG_A,
OUT_INTEROPLIB_OBJCLIB_A
)
installation_dir_dynamic = prepare_dir(install_path, "runtime", "lib", runtime)
install_files(
installation_dir_dynamic,
OUT_OBJC_INTERNAL_DYLIB,
OUT_OBJC_LANG_DYLIB,
OUT_INTEROPLIB_OBJCLIB_DYLIB
)
if IS_DARWIN:
change_install_names(
os.path.join(installation_dir_dynamic, "libobjc.internal.dylib"),
[]
)
change_install_names(
os.path.join(installation_dir_dynamic, "libobjc.lang.dylib"),
["libobjc.internal.dylib"]
)
change_install_names(
os.path.join(installation_dir_dynamic, "libinteroplib.objclib.dylib"),
[]
)
install_files(
prepare_dir(install_path, "modules", runtime),
OUT_OBJC_INTERNAL_CJO,
OUT_OBJC_LANG_CJO
)
LOG.info("end install interoplib for " + runtime + "\n")
else:
LOG.info("begin install objc-interop-gen...")
tools_bin_dir = prepare_dir(install_path, "tools", "bin")
install_file(tools_bin_dir, os.path.join(CMAKE_BUILD_DIR, "ObjCInteropGen"))
if IS_DARWIN:
INSTALLED_OBJC_INTEROP_GEN = os.path.join(tools_bin_dir, "ObjCInteropGen")
rpath = find_match(
subprocess.run(
["otool", "-l", INSTALLED_OBJC_INTEROP_GEN],
capture_output=True
).stdout.decode().splitlines(),
r"path (.*) \(offset \d*\)"
)
if rpath:
command(
"install_name_tool",
"-rpath",
rpath,
"@loader_path/../../third_party/llvm/lib",
INSTALLED_OBJC_INTEROP_GEN
)
else:
command(
"install_name_tool",
"-add_rpath",
"@loader_path/../../third_party/llvm/lib",
INSTALLED_OBJC_INTEROP_GEN
)
LOG.info("end install objc-interop-gen")
class BuildType(Enum):
"""CMAKE_BUILD_TYPE options"""
debug = 'Debug'
release = 'Release'
def __str__(self):
return self.name
def __repr__(self):
return str(self)
@staticmethod
def argparse(s):
try:
return BuildType[s]
except KeyError:
return s
def main():
"""build entry"""
clang_path = shutil.which('clang')
clang_pp_path = shutil.which('clang++')
if not clang_path:
LOG.error('clang is required to build cangjie compiler')
if not clang_pp_path:
LOG.error('clang++ is required to build cangjie compiler')
os.environ['CC'] = clang_path
os.environ['CXX'] = clang_pp_path
parser = argparse.ArgumentParser(description='build Objective-C binding generator or interoplib')
subparsers = parser.add_subparsers(help='sub command help')
parser_build = subparsers.add_parser('build', help='build Objective-C binding generator or interoplib')
parser_build.add_argument('-t',
'--build-type',
type=BuildType.argparse,
dest='build_type',
default=BuildType.release,
choices=list(BuildType),
help='select target build type')
parser_build.add_argument("--toml-dir", help="location of the toml.h C++ header file")
parser_build.add_argument("--llvm-src-dir", help="LLVM source directory")
parser_build.add_argument("--llvm-build-dir",
help="LLVM build directory that contains generated headers and compiled libraries"
)
parser_build.add_argument(
"--target", dest="target", type=str,
help="build interoplib for the specified target"
)
parser_build.add_argument(
"--target-lib", dest="target_lib", type=str,
help="when build interoplib, use the specified target triple"
)
parser_build.add_argument(
"--target-toolchain", dest="target_toolchain", type=str,
help="when build interoplib, use the tools under the given path to cross-compile (should point to bin directory)"
)
parser_build.add_argument(
"--target-sysroot", dest="target_sysroot", type=str,
help="when build interoplib, pass this argument to the compilers as --sysroot"
)
parser_build.set_defaults(func=build)
parser_install = subparsers.add_parser("install", help="install Objective-C binding generator or interoplib")
parser_install.add_argument(
"--target", dest="target", type=str,
help="install interoplib for the specified target"
)
parser_install.add_argument('--prefix',
dest='install_prefix',
help='target install prefix')
parser_install.set_defaults(func=install)
parser_clean = subparsers.add_parser("clean", help="clean for both Objective-C binding generator and interoplib")
parser_clean.set_defaults(func=clean)
args = parser.parse_args()
if not hasattr(args, 'func'):
args = parser.parse_args(['build'] + sys.argv[1:])
args.func(args)
if __name__ == '__main__':
LOG = init_log('root')
os.environ['LANG'] = "C.UTF-8"
main()