Files
icmtoolchain/src/icmtoolchain/native_build.py
T
MaXFeeD 8d1e2683c4 Migrating native.configurations to integrated configurations
- RuleSetHolder.bisect_properties to override existing properties, RuleSetHolder.remove_rules to cleanup unattended rules.
2025-06-21 14:19:59 +03:00

422 lines
16 KiB
Python

import os
import subprocess
from itertools import tee
from os.path import abspath, basename, exists, isdir, isfile, join, relpath
from typing import (Collection, Iterable, List, MutableSequence, NamedTuple,
Optional)
from . import GLOBALS, PROPERTIES
from .config import Config, FileConfig
from .language import MakeNativeData
from .native_setup import arch_to_abi, prepare_compiler_executable
from .output_directory import expand_paths
from .shell import debug, error, info, pretty_print, warn
from .utils import (copy_directory, copy_file, ensure_directory, ensure_file,
ensure_file_directory, ensure_not_whitespace,
get_all_files, remove_tree)
CODE_OK = 0
CODE_FAILED_NO_GCC = 1001
CODE_FAILED_INVALID_MANIFEST = 1002
CODE_DUPLICATE_NAME = 1003
CODE_INVALID_JSON = 1004
CODE_INVALID_PATH = 1005
class BuildTarget(NamedTuple):
directory: str
relative_directory: str
output_directory: str
manifest: MakeNativeData
stdincludes: MutableSequence[str]
def collect_stdincludes_directories(directories: Optional[Collection[str]]) -> List[str]:
stdincludes = list()
if not directories:
return stdincludes
for directory in directories:
stdincludes_directory = GLOBALS.MAKE_CONFIG.get_path(directory)
if not isdir(stdincludes_directory):
stdincludes_directory = GLOBALS.TOOLCHAIN_CONFIG.get_path(directory)
if not isdir(stdincludes_directory):
from .output_directory import get_config_directory
stdincludes_directory = join(get_config_directory(), directory)
if not isdir(stdincludes_directory):
warn(f"* Skipped non-existing stdincludes directory {directory!r}, please make sure that them exist!")
continue
has_directories = False
for filename in os.listdir(stdincludes_directory):
stdincludes_headers = join(stdincludes_directory, filename)
if isdir(stdincludes_headers):
stdincludes.append(stdincludes_headers)
has_directories = True
elif not has_directories and filename.endswith((".h", ".hpp")):
warn(f"* Header {filename} should be inside any of stdincludes directory, otherwise it will be ignored.")
return stdincludes
def get_manifest(directory: str) -> FileConfig:
return FileConfig(join(directory, "manifest"))
def get_name_from_manifest(directory: str) -> Optional[str]:
try:
return get_manifest(directory).get_value("shared.name", lambda: basename(directory))
except:
return None
def search_in_directory(parent: str, name: str) -> Optional[str]:
for dirpath, dirnames, filenames in os.walk(parent):
for relative_directory in dirnames:
path = join(dirpath, relative_directory)
if get_name_from_manifest(path) == name:
return path
def get_fakeso_directory(abi: str) -> str:
from .output_directory import get_temporary_directory
fakeso_directory = join(get_temporary_directory(), "ndk", abi, "fakeso")
ensure_directory(fakeso_directory)
return fakeso_directory
def add_fake_so(executable: str, abi: str, name: str) -> None:
file = join(get_fakeso_directory(abi), "lib" + name + ".so")
if not isfile(file):
from .output_directory import get_script_directory
result = subprocess.call([
executable, "-std=c++11",
join(get_script_directory(), "fakeso.cpp"),
"-shared", "-o", file
])
if result == 0:
debug(f"Created linking fake so {name!r} successfully")
else:
warn(f"Stubbing fake so failed with result {result}!")
RUNTIME_ARCHES = {
"arm64-v8a": "aarch64",
"x86_64": "x86-64"
}
def abi_to_runtime_architecture(abi: str) -> str:
if abi in RUNTIME_ARCHES:
return RUNTIME_ARCHES[abi]
return abi
def get_native_build_targets(directories: Iterable[MakeNativeData]) -> List[BuildTarget]:
targets = list()
for native_data in directories:
directory = GLOBALS.MAKE_CONFIG.get_path(native_data.relative_path)
output_directory = GLOBALS.MOD_STRUCTURE.new_build_target("native", native_data.output_path)
ensure_directory(output_directory)
stdincludes = collect_stdincludes_directories(list(native_data.stdincludes))
target = BuildTarget(directory, native_data.output_path, output_directory, native_data, stdincludes)
targets.append(target)
return targets
def compile_directory_with_gcc(directory: str, target_directory: str, target_so: str, abi: str, stdincludes: Collection[str], manifest: MakeNativeData) -> int:
info(f"* Compiling {manifest.shared_name!r} for {abi}")
soname = f"lib{manifest.shared_name}.so"
options = list(manifest.options)
if not options or len(options) == 0:
options = ["-std=c++11"]
debug(", ".join(options))
executable = prepare_compiler_executable(abi)
compiler_command = [executable, "-DANDROID_STL=c++_static"]
includes = list()
for stdincludes_directory in reversed(list(stdincludes)):
includes.append(f"-I{stdincludes_directory}")
dependencies = [f"-L{get_fakeso_directory(abi)}", "-landroid", "-lm", "-llog", "-ldl", "-lc"]
links = list(manifest.link)
if not "horizon" in links:
links.append("horizon")
for link in links:
add_fake_so(executable, abi, link)
dependencies.append(f"-l{link}")
# Always search for dependencies in current directory.
search_directory = abspath(join(directory, ".."))
for dependency in manifest.depends:
if dependency:
add_fake_so(executable, abi, dependency)
dependencies.append("-l" + dependency)
dependency_directory = search_in_directory(search_directory, dependency)
if dependency_directory:
try:
for include_directory in get_manifest(dependency_directory).obtain_list("shared.include"):
includes.append("-I" + join(dependency_directory, include_directory))
except KeyError:
pass
else:
warn(f"* Dependency directory {dependency} is not found, it will be skipped.")
for include in manifest.include:
includes.append("-I" + join(directory, include))
# Collect files and prepare output cache directories.
source_files = get_all_files(directory, extensions=(".cpp", ".c"))
preprocessed_directory = abspath(join(target_directory, "preprocessed", abi))
ensure_directory(preprocessed_directory)
object_directory = abspath(join(target_directory, "object", abi))
ensure_directory(object_directory)
# Preprocess to compile changed sources.
import filecmp
object_files = list()
object_position = 1
recompiled_count = 0
total_count = len(source_files)
for file in source_files:
relative_file = relpath(file, directory)
debug(f"Preprocessing {relative_file} ({object_position}/{total_count}){' ' * 48}", end="\r")
object_file = join(object_directory, relative_file) + ".o"
preprocessed_file = join(preprocessed_directory, relative_file)
tmp_preprocessed_file = preprocessed_file + ".tmp"
ensure_file_directory(preprocessed_file)
ensure_file_directory(object_file)
object_files.append(object_file)
result = subprocess.call(compiler_command + [
"-E", file, "-o", tmp_preprocessed_file
] + includes + options)
if result == CODE_OK:
if not isfile(preprocessed_file) or not isfile(object_file) \
or not filecmp.cmp(preprocessed_file, tmp_preprocessed_file):
if isfile(preprocessed_file):
os.remove(preprocessed_file)
os.rename(tmp_preprocessed_file, preprocessed_file)
if isfile(object_file):
os.remove(object_file)
debug(f"Compiling {relative_file} ({object_position}/{total_count}){' ' * 48}", end="\r")
result = max(result, subprocess.call(compiler_command + [
"-c", preprocessed_file, "-o", object_file
] + options + ([] if "64" in abi else ["-shared"])))
if result != CODE_OK:
if isfile(object_file):
os.remove(object_file)
overall_result = result
else:
recompiled_count += 1
else:
if isfile(object_file):
os.remove(object_file)
overall_result = result
object_position += 1
if overall_result != CODE_OK:
pretty_print()
return overall_result
debug(f"Recompiled {recompiled_count}/{total_count} files with result {overall_result} ({'OK' if overall_result == 0 else 'ERROR'}){' ' * 48}")
for link in manifest.link_static:
link_path = GLOBALS.MAKE_CONFIG.get_relative_path(join("static_libs", abi, link))
if isdir(link_path):
for object_file in get_all_files(link_path):
object_files.append(object_file)
elif exists(link_path):
object_files.append(link_path)
else:
warn(f"* Skipped static library {link}, because it was not exist.")
debug("Linking object files")
ensure_file(target_so)
linking_command = list()
linking_command += compiler_command
modified_objects = join(object_directory, "modified_objects.rsp")
with open(modified_objects, "w", encoding="utf-8") as modified:
modified.writelines(path.replace("\\", "\\\\") + "\n" for path in object_files)
linking_command.append("@" + modified_objects)
make_path = join(directory, "make.txt")
if isfile(make_path):
with open(make_path, encoding="utf-8") as file:
make = file.read()
if ensure_not_whitespace(make):
linking_command.append(make.rstrip())
linking_command.append("-shared")
linking_command.append("-Wl,-soname=" + soname)
if "-flto" in options:
debug("Linker time optimization is enabled")
linking_command += options
linking_command.append("-o")
linking_command.append(target_so)
linking_command += includes
linking_command += dependencies
return subprocess.call(linking_command)
def build_native_directories(directories: Iterable[MakeNativeData], directory_tuples: Iterable[tuple[str, Iterable[MakeNativeData]]], target_directory: str) -> int:
targets = get_native_build_targets(directories)
abi_targets = [(
directory[0],
iter(get_native_build_targets(directory[1]))
) for directory in directory_tuples]
for target in targets:
target_library_directory = join(target_directory, target.relative_directory)
soname = f"lib{target.manifest.shared_name}.so"
if target.manifest.keep_sources:
# Copy everything without built directories.
copy_directory(target.directory, target.output_directory, clear_destination=True)
remove_tree(join(target.output_directory, "so"))
os.remove(join(target.output_directory, soname))
else:
# Cleanup built directories and copy manifest.
remove_tree(target.output_directory)
copy_file(join(target.directory, "manifest"), join(target.output_directory, "manifest"))
# Also copy includes if necessary.
keep_includes = target.manifest.keep_includes
for include_path in target.manifest.include:
output_include_path = join(target.output_directory, include_path)
if keep_includes:
src_include_path = join(target.directory, include_path)
if isdir(src_include_path):
copy_directory(src_include_path, output_include_path, clear_destination=True)
else:
warn(f"* Shared headers folder {include_path!r} does not exist, check your build configuration!")
else:
remove_tree(output_include_path)
# Copy already prebuilt libraries, output path will be 'libname.so' or 'so/arch/libname.so'.
if exists(join(target.directory, ".precompiled")):
info(f"* Library directory {target.directory} skipped, because precompiled flag is set.")
libraries_count = 0
for abi, scoped_directories in abi_targets:
source_library = abspath(join(target.directory, "so", abi, soname))
if isfile(source_library):
target_library = abspath(join(target.output_directory, "so", abi_to_runtime_architecture(abi), soname))
copy_file(source_library, target_library)
libraries_count += 1
if libraries_count == 0:
source_library = abspath(join(target.directory, soname))
if isfile(source_library):
target_library = abspath(join(target.output_directory, soname))
copy_file(source_library, target_library)
libraries_count += 1
if libraries_count == 0:
warn(f"* Library directory {target.directory} should be precompiled, but there is no shared libraries.")
return CODE_FAILED_INVALID_MANIFEST
continue
# Compile library for requested ABIs.
overall_result = CODE_OK
for abi, scoped_directories in abi_targets:
GLOBALS.MAKE_CONFIG.bisect_properties(arch_to_abi(abi))
scoped_target = next(scoped_directories)
target_so = abspath(join(target.output_directory, soname)) if len(abi_targets) == 1 \
else abspath(join(target.output_directory, "so", abi_to_runtime_architecture(abi), soname))
overall_result += compile_directory_with_gcc(
scoped_target.directory,
target_library_directory,
target_so,
abi,
scoped_target.stdincludes,
scoped_target.manifest
)
if overall_result != 0:
break
GLOBALS.MAKE_CONFIG.remove_rules("native_architecture")
if overall_result != 0:
return overall_result
return CODE_OK
def compile_native(abis: Collection[str]) -> int:
from time import time
startup_millis = time()
overall_result = CODE_OK
target_directory = GLOBALS.MAKE_CONFIG.get_build_path("gcc")
ensure_directory(target_directory)
GLOBALS.MOD_STRUCTURE.cleanup_build_target("native")
stdincludes_directories = list()
stdincludes_toolchain = GLOBALS.TOOLCHAIN_CONFIG.get_relative_path("stdincludes")
if not isdir(stdincludes_toolchain):
from .output_directory import get_config_directory
stdincludes_toolchain = join(get_config_directory(), "stdincludes")
if isdir(stdincludes_toolchain):
stdincludes_directories.append(stdincludes_toolchain)
stdincludes_custom = GLOBALS.MAKE_CONFIG.get_relative_path("stdincludes")
if exists(stdincludes_custom):
stdincludes_directories.append(stdincludes_custom)
if not isdir(stdincludes_toolchain):
warn("Not found 'stdincludes', in most cases build will be failed, please install it via tasks.")
toolchain_config = None
if any(stdincludes_directories):
toolchain_config = Config()
toolchain_config.set_value("stdincludes", stdincludes_directories)
directories = GLOBALS.MAKE_CONFIG.iterate_native(defaults=toolchain_config)
directories, has_anything = tee(directories)
try:
next(has_anything)
except StopIteration:
GLOBALS.MOD_STRUCTURE.update_build_config_list("nativeDirs")
return 0
directory_tuples: Iterable[tuple[str, Iterable[MakeNativeData]]] = []
for abi in abis:
GLOBALS.MAKE_CONFIG.bisect_properties(arch_to_abi(abi))
scoped_directories = GLOBALS.MAKE_CONFIG.iterate_native(defaults=toolchain_config)
directory_tuples.append((abi, scoped_directories))
GLOBALS.MAKE_CONFIG.remove_rules("native_architecture")
overall_result = build_native_directories(directories, directory_tuples, target_directory)
GLOBALS.MOD_STRUCTURE.update_build_config_list("nativeDirs")
startup_millis = time() - startup_millis
if overall_result == CODE_OK:
pretty_print(f"Completed native build in {startup_millis:.2f}s!")
else:
error(f"Failed native build in {startup_millis:.2f}s with result {overall_result}.")
return overall_result
def copy_shared_objects(abis: Collection[str]) -> int:
shared_objects = GLOBALS.MAKE_CONFIG.iterate_shared_objects()
shared_objects, has_anything = tee(shared_objects)
try:
next(has_anything)
except StopIteration:
return 0
GLOBALS.MOD_STRUCTURE.cleanup_build_target("shared_object")
order = set()
debug(f"Copying shared objects")
overall_result = 0
for shared_object in shared_objects:
relative_path = shared_object.relative_path
for abi in abis:
GLOBALS.MAKE_CONFIG.bisect_properties(arch_to_abi(abi))
formatted_relative_path = relative_path.format(abi)
for shared_object_path in expand_paths(GLOBALS.MAKE_CONFIG.get_relative_path(formatted_relative_path)):
shared_object_name = basename(shared_object_path)
if shared_object_name in order:
warn(f"* Found duplicate shared object {formatted_relative_path}, overriding existing one...")
output_relative_file = join(abi_to_runtime_architecture(abi), shared_object_name)
output_file = GLOBALS.MOD_STRUCTURE.new_build_target("shared_object", output_relative_file)
copy_file(shared_object_path, output_file)
order.add(shared_object_name)
GLOBALS.MAKE_CONFIG.remove_rules("native_architecture")
if any(order):
output_directory = GLOBALS.MOD_STRUCTURE.get_target_output_directory("shared_object")
with open(join(output_directory, "order.txt"), "w", encoding="utf-8") as order_file:
for shared_object in order:
order_file.write(shared_object + "\n")
if overall_result == 0:
pretty_print(f"Completed including shared objects!")
else:
error(f"Failed to include shared objects with result {overall_result}.")
return overall_result