| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2015 The Android Open Source Project |
| # |
| # Licensed 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. |
| # |
| """Builds the NDK. |
| |
| Cleans old build artifacts, configures the required environment, determines |
| build goals, and invokes the build scripts. |
| """ |
| import argparse |
| import collections |
| import contextlib |
| import copy |
| |
| import inspect |
| import json |
| import logging |
| import multiprocessing |
| import os |
| from pathlib import Path |
| import pipes |
| import re |
| import shutil |
| import site |
| import stat |
| import subprocess |
| import sys |
| import textwrap |
| import traceback |
| from typing import ( |
| Any, |
| Callable, |
| ContextManager, |
| Dict, |
| Iterable, |
| Iterator, |
| List, |
| Set, |
| TextIO, |
| Tuple, |
| Union, |
| ) |
| |
| import ndk.abis |
| import ndk.ansi |
| import ndk.archive |
| import ndk.autoconf |
| import ndk.builds |
| import ndk.cmake |
| import ndk.config |
| import ndk.deps |
| import ndk.file |
| from ndk.hosts import Host |
| import ndk.notify |
| import ndk.paths |
| from ndk.paths import ANDROID_DIR, NDK_DIR |
| from ndk.platforms import MIN_API_LEVEL |
| import ndk.test.builder |
| import ndk.test.printers |
| import ndk.test.spec |
| import ndk.timer |
| from ndk.toolchains import ClangToolchain, CLANG_VERSION |
| import ndk.ui |
| import ndk.workqueue |
| from .pythonenv import ensure_python_environment |
| |
| |
| def get_version_string(build_number: str) -> str: |
| """Returns the version string for the current build.""" |
| return f"{ndk.config.major}.{ndk.config.hotfix}.{build_number}" |
| |
| |
| def purge_unwanted_files(ndk_dir: Path) -> None: |
| """Removes unwanted files from the NDK install path.""" |
| |
| for path in ndk.paths.walk(ndk_dir, directories=False): |
| if path.suffix == ".pyc": |
| path.unlink() |
| elif path.name == "Android.bp": |
| path.unlink() |
| |
| |
| def make_symlink(src: Path, dest: Path) -> None: |
| src.unlink(missing_ok=True) |
| if dest.is_absolute(): |
| src.symlink_to(Path(os.path.relpath(dest, src.parent))) |
| else: |
| src.symlink_to(dest) |
| |
| |
| def create_stub_entry_point(path: Path) -> None: |
| """Creates a stub "application" for the app bundle. |
| |
| App bundles must have at least one entry point in the Contents/MacOS |
| directory. We don't have a single entry point, and none of our executables |
| are useful if moved, so just put a welcome script in place that explains |
| that. |
| """ |
| path.parent.mkdir(exist_ok=True, parents=True) |
| path.write_text( |
| textwrap.dedent( |
| """\ |
| #!/bin/sh |
| echo "The Android NDK is installed to the Contents/NDK directory of this application bundle." |
| """ |
| ) |
| ) |
| path.chmod(0o755) |
| |
| |
| def create_plist(plist: Path, version: str, entry_point_name: str) -> None: |
| """Populates the NDK plist at the given location.""" |
| plist.write_text( |
| textwrap.dedent( |
| f"""\ |
| <?xml version="1.0" encoding="UTF-8"?> |
| <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| <plist version="1.0"> |
| <dict> |
| <key>CFBundleName</key> |
| <string>Android NDK</string> |
| <key>CFBundleDisplayName</key> |
| <string>Android NDK</string> |
| <key>CFBundleIdentifier</key> |
| <string>com.android.ndk</string> |
| <key>CFBundleVersion</key> |
| <string>{version}</string> |
| <key>CFBundlePackageType</key> |
| <string>APPL</string> |
| <key>CFBundleExecutable</key> |
| <string>{entry_point_name}</string> |
| </dict> |
| </plist> |
| """ |
| ) |
| ) |
| |
| |
| def create_signer_metadata(package_dir: Path) -> None: |
| """Populates the _codesign metadata directory for the ADRT signer. |
| |
| Args: |
| package_dir: Path to the root of the directory that will be zipped for |
| the signer. |
| """ |
| metadata_dir = package_dir / "_codesign" |
| metadata_dir.mkdir() |
| |
| # This directory can optionally contain a few pieces of metadata: |
| # |
| # filelist: For any jar files that need to be unpacked and signed. We have |
| # none. |
| # |
| # entitlements.xml: Defines any entitlements we need. No known, currently. |
| # |
| # volumename: The volume name for the DMG that the signer will create. |
| # |
| # See http://go/studio-signer for more information. |
| |
| volumename_file = metadata_dir / "volumename" |
| volumename_file.write_text(f"Android NDK {ndk.config.release}") |
| |
| |
| def make_app_bundle( |
| worker: ndk.workqueue.Worker, |
| zip_path: Path, |
| ndk_dir: Path, |
| build_number: str, |
| build_dir: Path, |
| ) -> None: |
| """Builds a macOS App Bundle of the NDK. |
| |
| The NDK is distributed in two forms on macOS: as a app bundle and in the |
| traditional layout. The traditional layout is needed by the SDK because AGP |
| and Studio expect the NDK to be contained one directory down in the |
| archive, which is not compatible with macOS bundles. The app bundle is |
| needed on macOS because we rely on rpaths, and executables using rpaths are |
| blocked by Gate Keeper as of macOS Catalina (10.15), except for references |
| within the same bundle. |
| |
| Information on the macOS bundle format can be found at |
| https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html. |
| |
| Args: |
| zip_path: The desired file path of the resultant zip file (without the |
| extension). |
| ndk_dir: The path to the NDK being bundled. |
| build_dir: The path to the top level build directory. |
| """ |
| worker.status = "Packaging MacOS App Bundle" |
| package_dir = build_dir / "bundle" |
| app_directory_name = f"AndroidNDK{build_number}.app" |
| bundle_dir = package_dir / app_directory_name |
| if package_dir.exists(): |
| shutil.rmtree(package_dir) |
| |
| contents_dir = bundle_dir / "Contents" |
| entry_point_name = "ndk" |
| create_stub_entry_point(contents_dir / "MacOS" / entry_point_name) |
| |
| bundled_ndk = contents_dir / "NDK" |
| shutil.copytree(ndk_dir, bundled_ndk) |
| |
| plist = contents_dir / "Info.plist" |
| create_plist(plist, get_version_string(build_number), entry_point_name) |
| |
| shutil.copy2(ndk_dir / "source.properties", package_dir / "source.properties") |
| create_signer_metadata(package_dir) |
| ndk.archive.make_zip( |
| zip_path, |
| package_dir, |
| [p.name for p in package_dir.iterdir()], |
| preserve_symlinks=True, |
| ) |
| |
| |
| def make_brtar( |
| worker: ndk.workqueue.Worker, |
| base_name: Path, |
| root_dir: Path, |
| base_dir: Path, |
| preserve_symlinks: bool, |
| ) -> None: |
| worker.status = "Packaging .tar.br" |
| ndk.archive.make_brtar( |
| base_name, root_dir, base_dir, preserve_symlinks=preserve_symlinks |
| ) |
| |
| |
| def make_zip( |
| worker: ndk.workqueue.Worker, |
| base_name: Path, |
| root_dir: Path, |
| paths: List[str], |
| preserve_symlinks: bool, |
| ) -> None: |
| worker.status = "Packaging .zip" |
| ndk.archive.make_zip( |
| base_name, root_dir, paths, preserve_symlinks=preserve_symlinks |
| ) |
| |
| |
| def package_ndk( |
| ndk_dir: Path, out_dir: Path, dist_dir: Path, host: Host, build_number: str |
| ) -> Path: |
| """Packages the built NDK for distribution. |
| |
| Args: |
| ndk_dir: Path to the built NDK. |
| out_dir: Path to use for constructing any intermediate outputs. |
| dist_dir: Path to place the built package in. |
| host: Host the given NDK was built for. |
| build_number: Build number to use in the package name. |
| """ |
| package_name = f"android-ndk-{build_number}-{host.tag}" |
| package_path = dist_dir / package_name |
| |
| purge_unwanted_files(ndk_dir) |
| |
| workqueue: ndk.workqueue.WorkQueue = ndk.workqueue.WorkQueue() |
| try: |
| if host == Host.Darwin: |
| workqueue.add_task( |
| make_app_bundle, |
| dist_dir / f"android-ndk-{build_number}-app-bundle", |
| ndk_dir, |
| build_number, |
| out_dir, |
| ) |
| workqueue.add_task( |
| make_brtar, |
| package_path, |
| ndk_dir.parent, |
| Path(ndk_dir.name), |
| preserve_symlinks=(host != Host.Windows64), |
| ) |
| workqueue.add_task( |
| make_zip, |
| package_path, |
| ndk_dir.parent, |
| [ndk_dir.name], |
| preserve_symlinks=(host != Host.Windows64), |
| ) |
| ndk.ui.finish_workqueue_with_ui(workqueue, ndk.ui.get_build_progress_ui) |
| finally: |
| workqueue.terminate() |
| workqueue.join() |
| # TODO: Treat the .tar.br archive as authoritative and return its path. |
| return package_path.with_suffix(".zip") |
| |
| |
| def build_ndk_tests(out_dir: Path, dist_dir: Path, args: argparse.Namespace) -> bool: |
| """Builds the NDK tests. |
| |
| Args: |
| out_dir: Build output directory. |
| dist_dir: Preserved artifact directory. |
| args: Parsed command line arguments. |
| |
| Returns: |
| True if all tests pass, else False. |
| """ |
| # The packaging step extracts all the modules to a known directory for |
| # packaging. This directory is not cleaned up after packaging, so we can |
| # reuse that for testing. |
| ndk_dir = ndk.paths.get_install_path(out_dir) |
| test_src_dir = ndk.paths.ndk_path("tests") |
| test_out_dir = out_dir / "tests" |
| |
| site.addsitedir(str(ndk_dir / "python-packages")) |
| |
| test_options = ndk.test.spec.TestOptions( |
| test_src_dir, |
| ndk_dir, |
| test_out_dir, |
| clean=True, |
| package_path=Path(dist_dir).joinpath("ndk-tests") |
| if args.package_tests |
| else None, |
| ) |
| |
| printer = ndk.test.printers.StdoutPrinter() |
| |
| test_spec = ndk.test.spec.TestSpec.load(ndk.paths.ndk_path("qa_config.json")) |
| builder = ndk.test.builder.TestBuilder(test_spec, test_options, printer) |
| |
| report = builder.build() |
| printer.print_summary(report) |
| |
| if not report.successful: |
| # Write out the result to logs/build_error.log so we can find the |
| # failure easily on the build server. |
| log_path = dist_dir / "logs" / "build_error.log" |
| with open(log_path, "a") as error_log: |
| error_log_printer = ndk.test.printers.FilePrinter(error_log) |
| error_log_printer.print_summary(report) |
| |
| return report.successful |
| |
| |
| def install_file(file_name: str, src_dir: Path, dst_dir: Path) -> None: |
| src_file = src_dir / file_name |
| dst_file = dst_dir / file_name |
| |
| print("Copying {} to {}...".format(src_file, dst_file)) |
| if src_file.is_dir(): |
| _install_dir(src_file, dst_file) |
| elif src_file.is_symlink(): |
| _install_symlink(src_file, dst_file) |
| else: |
| _install_file(src_file, dst_file) |
| |
| |
| def _install_dir(src_dir: Path, dst_dir: Path) -> None: |
| parent_dir = dst_dir.parent |
| if not parent_dir.exists(): |
| parent_dir.mkdir(parents=True) |
| shutil.copytree(src_dir, dst_dir, symlinks=True) |
| |
| |
| def _install_symlink(src_file: Path, dst_file: Path) -> None: |
| dirname = dst_file.parent |
| if not dirname.exists(): |
| dirname.mkdir(parents=True) |
| link_target = os.readlink(src_file) |
| os.symlink(link_target, dst_file) |
| |
| |
| def _install_file(src_file: Path, dst_file: Path) -> None: |
| dirname = dst_file.parent |
| if not dirname.exists(): |
| dirname.mkdir(parents=True) |
| # copy2 is just copy followed by copystat (preserves file metadata). |
| shutil.copy2(src_file, dst_file) |
| |
| |
| ALL_MODULE_TYPES: list[type[ndk.builds.Module]] = [] |
| |
| |
| def register(module_class: type[ndk.builds.Module]) -> type[ndk.builds.Module]: |
| ALL_MODULE_TYPES.append(module_class) |
| return module_class |
| |
| |
| @register |
| class Clang(ndk.builds.Module): |
| name = "clang" |
| install_path = Path("toolchains/llvm/prebuilt/{host}") |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| |
| @property |
| def notices(self) -> Iterator[Path]: |
| # TODO: Inject Host before this runs and remove this hack. |
| # Just skip the license checking for dev builds. Without this the build |
| # will fail because there's only a clang-dev for one of the hosts. |
| if CLANG_VERSION == "clang-dev": |
| return |
| for host in Host: |
| yield ClangToolchain.path_for_host(host) / "NOTICE" |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| install_path = self.get_install_path() |
| bin_dir = install_path / "bin" |
| |
| if install_path.exists(): |
| shutil.rmtree(install_path) |
| if not install_path.parent.exists(): |
| install_path.parent.mkdir(parents=True) |
| shutil.copytree( |
| ClangToolchain.path_for_host(self.host), |
| install_path, |
| symlinks=not self.host.is_windows, |
| ) |
| |
| # The prebuilt Linux Clangs include a bazel file for some other users. |
| # We don't need or test this interface so we shouldn't ship it. |
| if self.host is Host.Linux: |
| (install_path / "BUILD.bazel").unlink() |
| |
| # clang-4053586 was patched in the prebuilts directory to add the |
| # libc++ includes. These are almost certainly a different revision than |
| # the NDK libc++, and may contain local changes that the NDK's don't |
| # and vice versa. Best to just remove them for the time being since |
| # that returns to the previous behavior. |
| # https://github.com/android-ndk/ndk/issues/564#issuecomment-342307128 |
| shutil.rmtree(install_path / "include") |
| |
| if self.host is Host.Linux: |
| # The Linux toolchain wraps the compiler to inject some behavior |
| # for the platform. They aren't used for every platform and we want |
| # consistent behavior across platforms, and we also don't want the |
| # extra cost they incur (fork/exec is cheap, but CreateProcess is |
| # expensive), so remove them. |
| assert set(bin_dir.glob("*.real")) == { |
| bin_dir / "clang++.real", |
| bin_dir / "clang.real", |
| bin_dir / "clang-tidy.real", |
| } |
| (bin_dir / "clang++.real").unlink() |
| (bin_dir / "clang++").unlink() |
| (bin_dir / "clang-cl").unlink() |
| (bin_dir / "clang-tidy").unlink() |
| (bin_dir / "clang.real").rename(bin_dir / "clang") |
| (bin_dir / "clang-tidy.real").rename(bin_dir / "clang-tidy") |
| make_symlink(bin_dir / "clang++", Path("clang")) |
| |
| bin_ext = ".exe" if self.host.is_windows else "" |
| if self.host.is_windows: |
| # Remove LLD duplicates. We only need ld.lld. For non-Windows these |
| # are all symlinks so we can keep them (and *need* to keep lld |
| # since that's the real binary). |
| # http://b/74250510 |
| (bin_dir / f"ld64.lld{bin_ext}").unlink() |
| (bin_dir / f"lld{bin_ext}").unlink() |
| (bin_dir / f"lld-link{bin_ext}").unlink() |
| |
| install_clanglib = install_path / "lib64/clang" |
| linux_prebuilt_path = ClangToolchain.path_for_host(Host.Linux) |
| |
| # Remove unused python scripts. They are not installed for Windows. |
| if self.host != Host.Windows64: |
| python_bin_dir = install_path / "python3" / "bin" |
| python_files_to_remove = [ |
| "2to3*", |
| "easy_install*", |
| "idle*", |
| "pip*", |
| "pydoc*", |
| "python*-config", |
| ] |
| for file_pattern in python_files_to_remove: |
| for pyfile in python_bin_dir.glob(file_pattern): |
| pyfile.unlink() |
| |
| # Remove lldb-argdumper in site-packages. libc++ is not available there. |
| # People should use bin/lldb-argdumper instead. |
| for pylib in (install_path / "lib").glob("python*"): |
| (pylib / f"site-packages/lldb/lldb-argdumper{bin_ext}").unlink() |
| |
| if self.host != Host.Linux: |
| # We don't build target binaries as part of the Darwin or Windows |
| # build. These toolchains need to get these from the Linux |
| # prebuilts. |
| # |
| # The headers and libraries we care about are all in lib64/clang |
| # for both toolchains, and those two are intended to be identical |
| # between each host, so we can just replace them with the one from |
| # the Linux toolchain. |
| shutil.rmtree(install_clanglib) |
| shutil.copytree(linux_prebuilt_path / "lib64/clang", install_clanglib) |
| |
| # The Clang prebuilts have the platform toolchain libraries in |
| # lib64/clang. The libraries we want are in runtimes_ndk_cxx. |
| ndk_runtimes = linux_prebuilt_path / "runtimes_ndk_cxx" |
| for version_dir in install_clanglib.iterdir(): |
| dst_lib_dir = version_dir / "lib/linux" |
| shutil.rmtree(dst_lib_dir) |
| shutil.copytree(ndk_runtimes, dst_lib_dir) |
| |
| # Create empty libatomic.a stub libraries to keep -latomic working. |
| # This is needed for backwards compatibility and might be useful if |
| # upstream LLVM splits out the __atomic_* APIs from the builtins. |
| for arch in ndk.abis.ALL_ARCHITECTURES: |
| # Only the arch-specific subdir is on the linker search path. |
| subdir = { |
| ndk.abis.Arch("arm"): "arm", |
| ndk.abis.Arch("arm64"): "aarch64", |
| ndk.abis.Arch("x86"): "i386", |
| ndk.abis.Arch("x86_64"): "x86_64", |
| }[arch] |
| (dst_lib_dir / subdir / "libatomic.a").write_text( |
| textwrap.dedent( |
| """\ |
| /* The __atomic_* APIs are now in libclang_rt.builtins-*.a. They might |
| eventually be broken out into a separate library -- see llvm.org/D47606. */ |
| """ |
| ) |
| ) |
| |
| # Also remove the other libraries that we installed, but they were only |
| # installed on Linux. |
| if self.host == Host.Linux: |
| shutil.rmtree(install_path / "runtimes_ndk_cxx") |
| |
| # Remove CMake package files that should not be exposed. |
| # For some reason the LLVM install includes CMake modules that expose |
| # its internal APIs. We want to purge these so apps don't accidentally |
| # depend on them. See http://b/142327416 for more info. |
| shutil.rmtree(install_path / "lib64/cmake") |
| |
| # Remove libc++.a and libc++abi.a on Darwin. Now that these files are |
| # universal binaries, they break notarization. Maybe it is possible to |
| # fix notarization by using ditto to preserve APFS extended attributes. |
| # See https://developer.apple.com/forums/thread/126038. |
| if self.host == Host.Darwin: |
| (install_path / "lib64/libc++.a").unlink() |
| (install_path / "lib64/libc++abi.a").unlink() |
| |
| # Strip some large binaries and libraries. This is awkward, hand-crafted |
| # logic to select most of the biggest offenders, but could be |
| # greatly improved, although handling Mac, Windows, and Linux |
| # elegantly and consistently is a bit tricky. |
| strip_cmd = ClangToolchain(Host.current()).strip |
| for file in ndk.paths.walk(bin_dir, directories=False): |
| if not file.is_file() or file.is_symlink(): |
| continue |
| if Host.current().is_windows: |
| if file.suffix == ".exe": |
| subprocess.check_call([str(strip_cmd), str(file)]) |
| elif file.stat().st_size > 100000: |
| subprocess.check_call([str(strip_cmd), str(file)]) |
| for file in ndk.paths.walk(install_clanglib, directories=False): |
| if not file.is_file() or file.is_symlink(): |
| continue |
| if file.name == "lldb-server": |
| subprocess.check_call([str(strip_cmd), str(file)]) |
| if ( |
| file.name.startswith("libLLVM.") |
| or file.name.startswith("libclang.") |
| or file.name.startswith("libclang-cpp.") |
| or file.name.startswith("libLTO.") |
| or file.name.startswith("liblldb.") |
| ): |
| subprocess.check_call([str(strip_cmd), "--strip-unneeded", str(file)]) |
| |
| |
| def versioned_so(host: Host, lib: str, version: str) -> str: |
| """Returns the formatted versioned library for the given host. |
| |
| >>> versioned_so(Host.Darwin, 'libfoo', '0') |
| 'libfoo.0.dylib' |
| >>> versioned_so(Host.Linux, 'libfoo', '0') |
| 'libfoo.so.0' |
| """ |
| if host is Host.Darwin: |
| return f"{lib}.{version}.dylib" |
| if host is Host.Linux: |
| return f"{lib}.so.{version}" |
| raise ValueError(f"Unsupported host: {host}") |
| |
| |
| @register |
| class ShaderTools(ndk.builds.CMakeModule): |
| name = "shader-tools" |
| src = ANDROID_DIR / "external" / "shaderc" / "shaderc" |
| install_path = Path("shader-tools/{host}") |
| run_ctest = True |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| deps = {"clang"} |
| |
| @property |
| def notices(self) -> Iterator[Path]: |
| base = ANDROID_DIR / "external/shaderc" |
| shaderc_dir = base / "shaderc" |
| glslang_dir = base / "glslang" |
| spirv_dir = base / "spirv-headers" |
| yield shaderc_dir / "LICENSE" |
| yield shaderc_dir / "third_party/LICENSE.spirv-tools" |
| yield glslang_dir / "LICENSE.txt" |
| yield spirv_dir / "LICENSE" |
| |
| @property |
| def defines(self) -> Dict[str, str]: |
| gtest_dir = ANDROID_DIR / "external" / "googletest" |
| effcee_dir = ANDROID_DIR / "external" / "effcee" |
| re2_dir = ANDROID_DIR / "external" / "regex-re2" |
| spirv_headers_dir = self.src.parent / "spirv-headers" |
| defines = { |
| "SHADERC_EFFCEE_DIR": str(effcee_dir), |
| "SHADERC_RE2_DIR": str(re2_dir), |
| "SHADERC_GOOGLE_TEST_DIR": str(gtest_dir), |
| "SHADERC_THIRD_PARTY_ROOT_DIR": str(self.src.parent), |
| "EFFCEE_GOOGLETEST_DIR": str(gtest_dir), |
| "EFFCEE_RE2_DIR": str(re2_dir), |
| # SPIRV-Tools tests require effcee and re2. |
| # Don't enable RE2 testing because it's long and not useful to us. |
| "RE2_BUILD_TESTING": "OFF", |
| "SPIRV-Headers_SOURCE_DIR": str(spirv_headers_dir), |
| } |
| return defines |
| |
| @property |
| def flags(self) -> List[str]: |
| return super().flags + [ |
| "-Wno-unused-command-line-argument", |
| "-fno-rtti", |
| "-fno-exceptions", |
| ] |
| |
| @property |
| def ldflags(self) -> List[str]: |
| ldflags = super().ldflags |
| if self.host == Host.Linux: |
| # Our libc++.so.1 re-exports libc++abi, and it will be installed in |
| # the same directory as the executables. |
| ldflags += ["-Wl,-rpath,\\$ORIGIN"] |
| if self.host == Host.Windows64: |
| # TODO: The shaderc CMake files already pass these options for |
| # gcc+mingw but not for clang+mingw. See |
| # https://github.com/android/ndk/issues/1464. |
| ldflags += ["-static", "-static-libgcc", "-static-libstdc++"] |
| return ldflags |
| |
| @property |
| def env(self) -> Dict[str, str]: |
| # Sets path for libc++, for ctest. |
| if self.host == Host.Linux: |
| return {"LD_LIBRARY_PATH": str(self._libcxx_dir)} |
| return {} |
| |
| @property |
| def _libcxx_dir(self) -> Path: |
| return self.get_dep("clang").get_build_host_install() / "lib64" |
| |
| @property |
| def _libcxx(self) -> List[Path]: |
| path = self._libcxx_dir |
| if self.host == Host.Linux: |
| return [path / "libc++.so.1"] |
| return [] |
| |
| def install(self) -> None: |
| self.get_install_path().mkdir(parents=True, exist_ok=True) |
| ext = ".exe" if self.host.is_windows else "" |
| files_to_copy = [ |
| f"glslc{ext}", |
| f"spirv-as{ext}", |
| f"spirv-dis{ext}", |
| f"spirv-val{ext}", |
| f"spirv-cfg{ext}", |
| f"spirv-opt{ext}", |
| f"spirv-link{ext}", |
| f"spirv-reduce{ext}", |
| ] |
| scripts_to_copy = ["spirv-lesspipe.sh"] |
| |
| # Copy to install tree. |
| for src in files_to_copy + scripts_to_copy: |
| shutil.copy2( |
| self.builder.install_directory / "bin" / src, self.get_install_path() |
| ) |
| |
| # Symlink libc++ to install path. |
| for lib in self._libcxx: |
| symlink_name = self.get_install_path() / lib.name |
| make_symlink(symlink_name, lib) |
| |
| |
| @register |
| class Make(ndk.builds.CMakeModule): |
| name = "make" |
| install_path = Path("prebuilt/{host}") |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| src = ANDROID_DIR / "toolchain/make" |
| deps = {"clang"} |
| |
| @property |
| def notices(self) -> Iterator[Path]: |
| yield self.src / "COPYING" |
| |
| |
| @register |
| class Yasm(ndk.builds.AutoconfModule): |
| name = "yasm" |
| install_path = Path("prebuilt/{host}") |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| src = ANDROID_DIR / "toolchain/yasm" |
| |
| @property |
| def notices(self) -> Iterator[Path]: |
| files = [ |
| "Artistic.txt", |
| "BSD.txt", |
| "COPYING", |
| "GNU_GPL-2.0", |
| "GNU_LGPL-2.0", |
| ] |
| for name in files: |
| yield self.src / name |
| |
| |
| @register |
| class NdkWhich(ndk.builds.FileModule): |
| name = "ndk-which" |
| install_path = Path("prebuilt/{host}/bin/ndk-which") |
| src = NDK_DIR / "ndk-which" |
| |
| |
| @register |
| class Black(ndk.builds.LintModule): |
| name = "black" |
| |
| def run(self) -> None: |
| if not shutil.which("black"): |
| logging.warning( |
| "Skipping format-checking. black was not found on your path." |
| ) |
| return |
| subprocess.check_call(["black", "--check", "."]) |
| |
| |
| @register |
| class Pylint(ndk.builds.LintModule): |
| name = "pylint" |
| |
| def run(self) -> None: |
| if not shutil.which("pylint"): |
| logging.warning("Skipping linting. pylint was not found on your path.") |
| return |
| pylint = [ |
| "pylint", |
| "--rcfile=" + str(ANDROID_DIR / "ndk/pylintrc"), |
| "--score=n", |
| "build", |
| "ndk", |
| "tests", |
| ] |
| subprocess.check_call(pylint) |
| |
| |
| @register |
| class Mypy(ndk.builds.LintModule): |
| name = "mypy" |
| |
| def run(self) -> None: |
| if not shutil.which("mypy"): |
| logging.warning("Skipping type-checking. mypy was not found on your path.") |
| return |
| subprocess.check_call( |
| ["mypy", "--config-file", str(ANDROID_DIR / "ndk/mypy.ini"), "ndk"] |
| ) |
| |
| |
| @register |
| class PythonLint(ndk.builds.MetaModule): |
| name = "pythonlint" |
| deps = {"black", "mypy", "pylint"} |
| |
| |
| @register |
| class Toolbox(ndk.builds.Module): |
| name = "toolbox" |
| install_path = Path("prebuilt/{host}/bin") |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| notice = NDK_DIR / "sources/host-tools/toolbox/NOTICE" |
| |
| def build_exe(self, src: Path, name: str) -> None: |
| toolchain = ClangToolchain(self.host) |
| cmd = [ |
| str(toolchain.cc), |
| "-s", |
| "-o", |
| str(self.intermediate_out_dir / f"{name}.exe"), |
| str(src), |
| ] + toolchain.flags |
| subprocess.run(cmd, check=True) |
| |
| def build(self) -> None: |
| if not self.host.is_windows: |
| print(f"Nothing to do for {self.host}") |
| return |
| |
| self.intermediate_out_dir.mkdir(parents=True, exist_ok=True) |
| |
| src_dir = NDK_DIR / "sources/host-tools/toolbox" |
| self.build_exe(src_dir / "echo_win.c", "echo") |
| self.build_exe(src_dir / "cmp_win.c", "cmp") |
| |
| def install(self) -> None: |
| if not self.host.is_windows: |
| print(f"Nothing to do for {self.host}") |
| return |
| |
| install_dir = self.get_install_path() |
| install_dir.mkdir(parents=True, exist_ok=True) |
| |
| shutil.copy2(self.intermediate_out_dir / "echo.exe", install_dir) |
| shutil.copy2(self.intermediate_out_dir / "cmp.exe", install_dir) |
| |
| |
| def install_exe(out_dir: Path, install_dir: Path, name: str, host: Host) -> None: |
| ext = ".exe" if host.is_windows else "" |
| exe_name = name + ext |
| src = out_dir / exe_name |
| dst = install_dir / exe_name |
| |
| install_dir.mkdir(parents=True, exist_ok=True) |
| shutil.copy2(src, dst) |
| |
| |
| def make_linker_script(path: Path, libs: List[str]) -> None: |
| ndk.file.write_file(path, "INPUT({})\n".format(" ".join(libs))) |
| |
| |
| @register |
| class Libcxx(ndk.builds.Module): |
| name = "libc++" |
| src = ANDROID_DIR / "toolchain/llvm-project/libcxx" |
| install_path = Path("sources/cxx-stl/llvm-libc++") |
| notice = src / "LICENSE.TXT" |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| deps = { |
| "base-toolchain", |
| "ndk-build", |
| "ndk-build-shortcut", |
| } |
| |
| @property |
| def obj_out(self) -> Path: |
| return self.out_dir / "libcxx" / "obj" |
| |
| @property |
| def lib_out(self) -> Path: |
| return self.out_dir / "libcxx" / "libs" |
| |
| def build(self) -> None: |
| ndk_build = self.get_dep("ndk-build").get_build_host_install() / "ndk-build" |
| |
| android_mk = self.src / "Android.mk" |
| application_mk = self.src / "Application.mk" |
| |
| build_cmd = [ |
| "bash", |
| str(ndk_build), |
| f"-j{multiprocessing.cpu_count()}", |
| "V=1", |
| # Since nothing in this build depends on libc++_static, we need to |
| # name it to force it to build. |
| "APP_MODULES=c++_shared c++_static", |
| # Tell ndk-build where all of our makefiles are and where outputs |
| # should go. The defaults in ndk-build are only valid if we have a |
| # typical ndk-build layout with a jni/{Android,Application}.mk. |
| "NDK_PROJECT_PATH=null", |
| f"APP_BUILD_SCRIPT={android_mk}", |
| f"NDK_APPLICATION_MK={application_mk}", |
| f"NDK_OUT={self.obj_out}", |
| f"NDK_LIBS_OUT={self.lib_out}", |
| # Make sure we don't pick up a cached copy. |
| "LIBCXX_FORCE_REBUILD=true", |
| ] |
| |
| print("Running: " + " ".join([pipes.quote(arg) for arg in build_cmd])) |
| subprocess.check_call(build_cmd) |
| |
| def install(self) -> None: |
| """Installs headers and makefiles. |
| |
| The libraries are installed separately, by the Toolchain module.""" |
| install_root = self.get_install_path() |
| |
| if install_root.exists(): |
| shutil.rmtree(install_root) |
| install_root.mkdir(parents=True) |
| |
| shutil.copy2(self.src / "Android.mk", install_root) |
| # TODO: Use the includes from sysroot. |
| shutil.copytree(self.src / "include", install_root / "include") |
| |
| |
| @register |
| class Platforms(ndk.builds.Module): |
| name = "platforms" |
| install_path = Path("platforms") |
| |
| deps = { |
| "clang", |
| } |
| |
| min_supported_api = MIN_API_LEVEL |
| |
| # These API levels had no new native APIs. The contents of these platforms |
| # directories would be identical to the previous extant API level, so they |
| # are not included in the NDK to save space. |
| skip_apis = (20, 25) |
| |
| # Shared with the sysroot, though the sysroot NOTICE actually includes a |
| # lot more licenses. Platforms and Sysroot are essentially a single |
| # component that is split into two directories only temporarily, so this |
| # will be the end state when we merge the two anyway. |
| notice = ANDROID_DIR / "prebuilts/ndk/platform/sysroot/NOTICE" |
| |
| intermediate_module = True |
| |
| prebuilts_path = ANDROID_DIR / "prebuilts/ndk/platform" |
| |
| @staticmethod |
| def src_path(*args: str) -> Path: |
| return ndk.paths.android_path("development/ndk/platforms", *args) |
| |
| def llvm_tool(self, tool: str) -> Path: |
| path = Path(self.get_dep("clang").get_build_host_install()) |
| return path / f"bin/{tool}" |
| |
| @staticmethod |
| def libdir_name(arch: ndk.abis.Arch) -> str: |
| if arch == "x86_64": |
| return "lib64" |
| return "lib" |
| |
| def get_apis(self) -> List[int]: |
| apis: List[int] = [] |
| for path in (self.prebuilts_path / "platforms").iterdir(): |
| name = path.name |
| if not name.startswith("android-"): |
| continue |
| |
| _, api_str = name.split("-") |
| try: |
| api = int(api_str) |
| if api >= self.min_supported_api: |
| apis.append(api) |
| except ValueError as ex: |
| # Codenamed release like android-O, android-O-MR1, etc. |
| # Codenamed APIs are not supported, since having |
| # non-integer API directories breaks all kinds of tools, we |
| # rename them when we check them in. |
| raise ValueError( |
| f"No codenamed API is allowed: {api_str}\n" |
| "Use the update_platform.py tool from the " |
| "platform/prebuilts/ndk dev branch to remove or rename it." |
| ) from ex |
| |
| return sorted(apis) |
| |
| @staticmethod |
| def get_arches(api: Union[int, str]) -> list[ndk.abis.Arch]: |
| arches = [ndk.abis.Arch("arm"), ndk.abis.Arch("x86")] |
| # All codenamed APIs are at 64-bit capable. |
| if isinstance(api, str) or api >= 21: |
| arches.extend([ndk.abis.Arch("arm64"), ndk.abis.Arch("x86_64")]) |
| return arches |
| |
| def get_build_cmd( |
| self, |
| dst: Path, |
| srcs: List[Path], |
| api: int, |
| arch: ndk.abis.Arch, |
| build_number: Union[int, str], |
| ) -> List[str]: |
| libc_includes = ndk.paths.ANDROID_DIR / "bionic/libc" |
| arch_common_includes = libc_includes / "arch-common/bionic" |
| |
| cc = self.llvm_tool("clang") |
| |
| args = [ |
| str(cc), |
| "-target", |
| ndk.abis.clang_target(arch, api), |
| "--sysroot", |
| str(self.prebuilts_path / "sysroot"), |
| "-fuse-ld=lld", |
| f"-I{libc_includes}", |
| f"-I{arch_common_includes}", |
| f"-DPLATFORM_SDK_VERSION={api}", |
| f'-DABI_NDK_VERSION="{ndk.config.release}"', |
| f'-DABI_NDK_BUILD_NUMBER="{build_number}"', |
| "-O2", |
| "-fpic", |
| "-Wl,-r", |
| "-no-pie", |
| "-nostdlib", |
| "-Wa,--noexecstack", |
| "-Wl,-z,noexecstack", |
| "-o", |
| str(dst), |
| ] + [str(src) for src in srcs] |
| |
| if arch == ndk.abis.Arch("arm64"): |
| args.append("-mbranch-protection=standard") |
| |
| return args |
| |
| def check_elf_note(self, obj_file: Path) -> None: |
| # readelf is a cross platform tool, so arch doesn't matter. |
| readelf = self.llvm_tool("llvm-readelf") |
| out = subprocess.check_output([str(readelf), "--notes", str(obj_file)]) |
| if "Android" not in out.decode("utf-8"): |
| raise RuntimeError("{} does not contain NDK ELF note".format(obj_file)) |
| |
| def build_crt_object( |
| self, |
| dst: Path, |
| srcs: List[Path], |
| api: int, |
| arch: ndk.abis.Arch, |
| build_number: Union[int, str], |
| defines: List[str], |
| ) -> None: |
| cc_args = self.get_build_cmd(dst, srcs, api, arch, build_number) |
| cc_args.extend(defines) |
| |
| print("Running: " + " ".join([pipes.quote(arg) for arg in cc_args])) |
| subprocess.check_call(cc_args) |
| |
| def build_crt_objects( |
| self, |
| dst_dir: Path, |
| api: int, |
| arch: ndk.abis.Arch, |
| build_number: Union[int, str], |
| ) -> None: |
| src_dir = ndk.paths.android_path("bionic/libc/arch-common/bionic") |
| crt_brand = ndk.paths.ndk_path("sources/crt/crtbrand.S") |
| |
| objects = { |
| "crtbegin_dynamic.o": [ |
| src_dir / "crtbegin.c", |
| crt_brand, |
| ], |
| "crtbegin_so.o": [ |
| src_dir / "crtbegin_so.c", |
| crt_brand, |
| ], |
| "crtbegin_static.o": [ |
| src_dir / "crtbegin.c", |
| crt_brand, |
| ], |
| "crtend_android.o": [ |
| src_dir / "crtend.S", |
| ], |
| "crtend_so.o": [ |
| src_dir / "crtend_so.S", |
| ], |
| } |
| |
| for name, srcs in objects.items(): |
| dst_path = dst_dir / name |
| defs = [] |
| if name == "crtbegin_static.o": |
| # libc.a is always the latest version, so ignore the API level |
| # setting for crtbegin_static. |
| defs.append("-D_FORCE_CRT_ATFORK") |
| self.build_crt_object(dst_path, srcs, api, arch, build_number, defs) |
| if name.startswith("crtbegin"): |
| self.check_elf_note(dst_path) |
| |
| def build(self) -> None: |
| build_dir = self.out_dir / self.install_path |
| if build_dir.exists(): |
| shutil.rmtree(build_dir) |
| |
| apis = self.get_apis() |
| platforms_meta = json.loads( |
| ndk.file.read_file(ndk.paths.ndk_path("meta/platforms.json")) |
| ) |
| max_sysroot_api = apis[-1] |
| max_meta_api = platforms_meta["max"] |
| if max_sysroot_api != max_meta_api: |
| raise RuntimeError( |
| f"API {max_sysroot_api} is the newest API level in the " |
| "sysroot but does not match meta/platforms.json max of " |
| f"{max_meta_api}" |
| ) |
| if max_sysroot_api not in platforms_meta["aliases"].values(): |
| raise RuntimeError( |
| f"API {max_sysroot_api} is the newest API level in the " |
| "sysroot but has no alias in meta/platforms.json." |
| ) |
| for api in apis: |
| if api in self.skip_apis: |
| continue |
| |
| platform = "android-{}".format(api) |
| for arch in self.get_arches(api): |
| arch_name = "arch-{}".format(arch) |
| dst_dir = build_dir / platform / arch_name |
| dst_dir.mkdir(parents=True) |
| assert self.context is not None |
| self.build_crt_objects(dst_dir, api, arch, self.context.build_number) |
| |
| def install(self) -> None: |
| build_dir = self.out_dir / self.install_path |
| install_dir = self.get_install_path() |
| |
| if install_dir.exists(): |
| shutil.rmtree(install_dir) |
| install_dir.mkdir(parents=True) |
| |
| for api in self.get_apis(): |
| if api in self.skip_apis: |
| continue |
| |
| # Copy shared libraries from prebuilts/ndk/platform/platforms. |
| platform = "android-{}".format(api) |
| platform_src = self.prebuilts_path / "platforms" / platform |
| platform_dst = install_dir / "android-{}".format(api) |
| shutil.copytree(platform_src, platform_dst) |
| |
| for arch in self.get_arches(api): |
| arch_name = "arch-{}".format(arch) |
| triple = ndk.abis.arch_to_triple(arch) |
| |
| # Install static libraries from prebuilts/ndk/platform/sysroot. |
| # TODO: Determine if we can change the build system to use the |
| # libraries directly from the sysroot directory rather than |
| # duplicating all the libraries in platforms. |
| lib_dir = self.prebuilts_path / "sysroot/usr/lib" / triple |
| libdir_name = self.libdir_name(arch) |
| lib_dir_dst = install_dir / platform / arch_name / "usr" / libdir_name |
| for name in os.listdir(lib_dir): |
| lib_src = lib_dir / name |
| lib_dst = lib_dir_dst / name |
| shutil.copy2(lib_src, lib_dst) |
| |
| if libdir_name == "lib64": |
| # The Clang driver won't accept a sysroot that contains |
| # only a lib64. An empty lib dir is enough to convince it. |
| (install_dir / platform / arch_name / "usr/lib").mkdir(parents=True) |
| |
| # Install the CRT objects that we just built. |
| obj_dir = build_dir / platform / arch_name |
| for name in os.listdir(obj_dir): |
| obj_src = obj_dir / name |
| obj_dst = lib_dir_dst / name |
| shutil.copy2(obj_src, obj_dst) |
| |
| # https://github.com/android-ndk/ndk/issues/372 |
| for root, dirs, files in os.walk(install_dir): |
| if not files and not dirs: |
| with open(Path(root) / ".keep_dir", "w") as keep_file: |
| keep_file.write("This file forces git to keep the directory.") |
| |
| |
| @register |
| class LibShaderc(ndk.builds.Module): |
| name = "libshaderc" |
| install_path = Path("sources/third_party/shaderc") |
| src = ANDROID_DIR / "external/shaderc" |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| |
| @property |
| def notices(self) -> Iterator[Path]: |
| shaderc_dir = self.src / "shaderc" |
| glslang_dir = self.src / "glslang" |
| yield shaderc_dir / "LICENSE" |
| yield glslang_dir / "LICENSE.txt" |
| yield shaderc_dir / "third_party/LICENSE.spirv-tools" |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| copies = [ |
| { |
| "source_dir": str(self.src / "shaderc"), |
| "dest_dir": "", |
| "files": [ |
| "Android.mk", |
| "libshaderc/Android.mk", |
| "libshaderc_util/Android.mk", |
| "third_party/Android.mk", |
| "utils/update_build_version.py", |
| "CHANGES", |
| ], |
| "dirs": [ |
| "libshaderc/include", |
| "libshaderc/src", |
| "libshaderc_util/include", |
| "libshaderc_util/src", |
| ], |
| }, |
| { |
| "source_dir": str(self.src / "spirv-tools"), |
| "dest_dir": "third_party/spirv-tools", |
| "files": [ |
| "utils/generate_grammar_tables.py", |
| "utils/generate_language_headers.py", |
| "utils/generate_registry_tables.py", |
| "utils/update_build_version.py", |
| "Android.mk", |
| "CHANGES", |
| ], |
| "dirs": ["include", "source"], |
| }, |
| { |
| "source_dir": str(self.src / "spirv-headers"), |
| "dest_dir": "third_party/spirv-tools/external/spirv-headers", |
| "dirs": ["include"], |
| "files": [ |
| "include/spirv/1.0/spirv.py", |
| "include/spirv/1.1/spirv.py", |
| "include/spirv/1.2/spirv.py", |
| "include/spirv/uinified1/spirv.py", |
| ], |
| }, |
| { |
| "source_dir": str(self.src / "glslang"), |
| "dest_dir": "third_party/glslang", |
| "files": [ |
| "Android.mk", |
| "glslang/OSDependent/osinclude.h", |
| # Build version info is generated from the CHANGES.md file. |
| "CHANGES.md", |
| "build_info.h.tmpl", |
| "build_info.py", |
| "StandAlone/DirStackFileIncluder.h", |
| "StandAlone/ResourceLimits.h", |
| ], |
| "dirs": [ |
| "SPIRV", |
| "OGLCompilersDLL", |
| "glslang/CInterface", |
| "glslang/GenericCodeGen", |
| "hlsl", |
| "glslang/HLSL", |
| "glslang/Include", |
| "glslang/MachineIndependent", |
| "glslang/OSDependent/Unix", |
| "glslang/Public", |
| ], |
| }, |
| ] |
| |
| default_ignore_patterns = shutil.ignore_patterns( |
| "*CMakeLists.txt", "*.py", "*test.h", "*test.cc" |
| ) |
| |
| install_dir = self.get_install_path() |
| if install_dir.exists(): |
| shutil.rmtree(install_dir) |
| |
| for properties in copies: |
| source_dir = properties["source_dir"] |
| assert isinstance(source_dir, str) |
| assert isinstance(properties["dest_dir"], str) |
| dest_dir = install_dir / properties["dest_dir"] |
| for d in properties["dirs"]: |
| assert isinstance(d, str) |
| src = Path(source_dir) / d |
| dst = Path(dest_dir) / d |
| print(src, " -> ", dst) |
| shutil.copytree(src, dst, ignore=default_ignore_patterns) |
| for f in properties["files"]: |
| print(source_dir, ":", dest_dir, ":", f) |
| # Only copy if the source file exists. That way |
| # we can update this script in anticipation of |
| # source files yet-to-come. |
| assert isinstance(f, str) |
| if (Path(source_dir) / f).exists(): |
| install_file(f, Path(source_dir), Path(dest_dir)) |
| else: |
| print(source_dir, ":", dest_dir, ":", f, "SKIPPED") |
| |
| |
| @register |
| class CpuFeatures(ndk.builds.PackageModule): |
| name = "cpufeatures" |
| install_path = Path("sources/android/cpufeatures") |
| src = NDK_DIR / "sources/android/cpufeatures" |
| |
| |
| @register |
| class NativeAppGlue(ndk.builds.PackageModule): |
| name = "native_app_glue" |
| install_path = Path("sources/android/native_app_glue") |
| src = NDK_DIR / "sources/android/native_app_glue" |
| |
| |
| @register |
| class NdkHelper(ndk.builds.PackageModule): |
| name = "ndk_helper" |
| install_path = Path("sources/android/ndk_helper") |
| src = NDK_DIR / "sources/android/ndk_helper" |
| |
| |
| @register |
| class Gtest(ndk.builds.PackageModule): |
| name = "gtest" |
| install_path = Path("sources/third_party/googletest") |
| src = ANDROID_DIR / "external/googletest/googletest" |
| |
| def install(self) -> None: |
| super().install() |
| # Docs are moved to top level directory. |
| shutil.rmtree(self.get_install_path() / "docs") |
| |
| |
| @register |
| class Sysroot(ndk.builds.Module): |
| name = "sysroot" |
| install_path = Path("sysroot") |
| notice = ANDROID_DIR / "prebuilts/ndk/platform/sysroot/NOTICE" |
| intermediate_module = True |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| install_path = self.get_install_path() |
| if install_path.exists(): |
| shutil.rmtree(install_path) |
| path = ndk.paths.android_path("prebuilts/ndk/platform/sysroot") |
| shutil.copytree(path, install_path) |
| if self.host is not Host.Linux: |
| # linux/netfilter has some headers with names that differ only |
| # by case, which can't be extracted to a case-insensitive |
| # filesystem, which are the defaults for Darwin and Windows :( |
| # |
| # There isn't really a good way to decide which of these to |
| # keep and which to remove. The capitalized versions expose |
| # different APIs, but we can't keep both. So far no one has |
| # filed bugs about needing either API, so let's just dedup them |
| # consistently and we can change that if we hear otherwise. |
| remove_paths = [ |
| "usr/include/linux/netfilter_ipv4/ipt_ECN.h", |
| "usr/include/linux/netfilter_ipv4/ipt_TTL.h", |
| "usr/include/linux/netfilter_ipv6/ip6t_HL.h", |
| "usr/include/linux/netfilter/xt_CONNMARK.h", |
| "usr/include/linux/netfilter/xt_DSCP.h", |
| "usr/include/linux/netfilter/xt_MARK.h", |
| "usr/include/linux/netfilter/xt_RATEEST.h", |
| "usr/include/linux/netfilter/xt_TCPMSS.h", |
| ] |
| for remove_path in remove_paths: |
| os.remove(install_path / remove_path) |
| |
| ndk_version_h_path = install_path / "usr/include/android/ndk-version.h" |
| with open(ndk_version_h_path, "w") as ndk_version_h: |
| major = ndk.config.major |
| minor = ndk.config.hotfix |
| beta = ndk.config.beta |
| canary = "1" if ndk.config.canary else "0" |
| assert self.context is not None |
| |
| ndk_version_h.write( |
| textwrap.dedent( |
| f"""\ |
| #pragma once |
| |
| /** |
| * Set to 1 if this is an NDK, unset otherwise. See |
| * https://android.googlesource.com/platform/bionic/+/master/docs/defines.md. |
| */ |
| #define __ANDROID_NDK__ 1 |
| |
| /** |
| * Major version of this NDK. |
| * |
| * For example: 16 for r16. |
| */ |
| #define __NDK_MAJOR__ {major} |
| |
| /** |
| * Minor version of this NDK. |
| * |
| * For example: 0 for r16 and 1 for r16b. |
| */ |
| #define __NDK_MINOR__ {minor} |
| |
| /** |
| * Set to 0 if this is a release build, or 1 for beta 1, |
| * 2 for beta 2, and so on. |
| */ |
| #define __NDK_BETA__ {beta} |
| |
| /** |
| * Build number for this NDK. |
| * |
| * For a local development build of the NDK, this is -1. |
| */ |
| #define __NDK_BUILD__ {self.context.build_number} |
| |
| /** |
| * Set to 1 if this is a canary build, 0 if not. |
| */ |
| #define __NDK_CANARY__ {canary} |
| """ |
| ) |
| ) |
| |
| |
| def write_clang_shell_script( |
| wrapper_path: Path, clang_name: str, flags: List[str] |
| ) -> None: |
| with open(wrapper_path, "w") as wrapper: |
| wrapper.write( |
| textwrap.dedent( |
| """\ |
| #!/usr/bin/env bash |
| bin_dir=`dirname "$0"` |
| if [ "$1" != "-cc1" ]; then |
| "$bin_dir/{clang}" {flags} "$@" |
| else |
| # Target is already an argument. |
| "$bin_dir/{clang}" "$@" |
| fi |
| """.format( |
| clang=clang_name, flags=" ".join(flags) |
| ) |
| ) |
| ) |
| |
| mode = os.stat(wrapper_path).st_mode |
| os.chmod(wrapper_path, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) |
| |
| |
| def write_clang_batch_script( |
| wrapper_path: Path, clang_name: str, flags: List[str] |
| ) -> None: |
| with open(wrapper_path, "w") as wrapper: |
| wrapper.write( |
| textwrap.dedent( |
| """\ |
| @echo off |
| setlocal |
| call :find_bin |
| if "%1" == "-cc1" goto :L |
| |
| set "_BIN_DIR=" && "%_BIN_DIR%{clang}" {flags} %* |
| if ERRORLEVEL 1 exit /b 1 |
| goto :done |
| |
| :L |
| rem Target is already an argument. |
| set "_BIN_DIR=" && "%_BIN_DIR%{clang}" %* |
| if ERRORLEVEL 1 exit /b 1 |
| goto :done |
| |
| :find_bin |
| rem Accommodate a quoted arg0, e.g.: "clang" |
| rem https://github.com/android-ndk/ndk/issues/616 |
| set _BIN_DIR=%~dp0 |
| exit /b |
| |
| :done |
| """.format( |
| clang=clang_name, flags=" ".join(flags) |
| ) |
| ) |
| ) |
| |
| |
| def write_clang_wrapper( |
| install_dir: Path, api: int, triple: str, is_windows: bool |
| ) -> None: |
| """Writes a target-specific Clang wrapper. |
| |
| This wrapper can be used to target the given architecture/API combination |
| without needing to specify -target. These obviate the need for standalone |
| toolchains. |
| |
| Ideally these would be symlinks rather than wrapper scripts to avoid the |
| unnecessary indirection (Clang will infer its default target based on |
| argv[0]), but the SDK manager can't install symlinks and Windows only |
| allows administrators to create them. |
| """ |
| exe_suffix = ".exe" if is_windows else "" |
| |
| if triple.startswith("arm-linux"): |
| triple = "armv7a-linux-androideabi" |
| |
| wrapper_path = install_dir / "{}{}-clang".format(triple, api) |
| wrapperxx_path = wrapper_path.parent / (wrapper_path.name + "++") |
| |
| flags = ["--target={}{}".format(triple, api)] |
| |
| # TODO: Hoist into the driver. |
| if triple.startswith("i686") and api < 24: |
| flags.append("-mstackrealign") |
| |
| # Write shell scripts even for Windows to support WSL and Cygwin. |
| write_clang_shell_script(wrapper_path, "clang" + exe_suffix, flags) |
| write_clang_shell_script(wrapperxx_path, "clang++" + exe_suffix, flags) |
| if is_windows: |
| write_clang_batch_script( |
| wrapper_path.with_suffix(".cmd"), "clang" + exe_suffix, flags |
| ) |
| write_clang_batch_script( |
| wrapperxx_path.with_suffix(".cmd"), "clang++" + exe_suffix, flags |
| ) |
| |
| |
| @register |
| class BaseToolchain(ndk.builds.Module): |
| """The subset of the toolchain needed to build other toolchain components. |
| |
| libc++ is built using this toolchain, and the full toolchain requires |
| libc++. The toolchain is split into BaseToolchain and Toolchain to break |
| the cyclic dependency. |
| """ |
| |
| name = "base-toolchain" |
| # This is installed to the Clang location to avoid migration pain. |
| install_path = Path("toolchains/llvm/prebuilt/{host}") |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| deps = { |
| "clang", |
| "make", |
| "platforms", |
| "sysroot", |
| "system-stl", |
| "yasm", |
| } |
| |
| @property |
| def notices(self) -> Iterator[Path]: |
| yield from Clang().notices |
| yield from Yasm().notices |
| yield from Platforms().notices |
| yield from Sysroot().notices |
| yield from SystemStl().notices |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| install_dir = self.get_install_path() |
| yasm_dir = self.get_dep("yasm").get_install_path() |
| platforms_dir = self.get_dep("platforms").get_install_path() |
| sysroot_dir = self.get_dep("sysroot").get_install_path() |
| system_stl_dir = self.get_dep("system-stl").get_install_path() |
| |
| shutil.copytree(sysroot_dir, install_dir / "sysroot", dirs_exist_ok=True) |
| |
| exe = ".exe" if self.host.is_windows else "" |
| shutil.copy2( |
| yasm_dir / "bin" / ("yasm" + exe), |
| install_dir / "bin", |
| ) |
| |
| bin_dir = Path(install_dir) / "bin" |
| lld = bin_dir / f"ld.lld{exe}" |
| new_bin_ld = bin_dir / f"ld{exe}" |
| |
| if self.host.is_windows: |
| shutil.copyfile(lld, new_bin_ld) |
| shutil.copystat(lld, new_bin_ld) |
| else: |
| # This reduces the size of the NDK by 60M on non-Windows. |
| os.symlink(lld.name, new_bin_ld) |
| |
| platforms = self.get_dep("platforms") |
| assert isinstance(platforms, Platforms) |
| for api in platforms.get_apis(): |
| if api in Platforms.skip_apis: |
| continue |
| |
| platform = "android-{}".format(api) |
| for arch in platforms.get_arches(api): |
| triple = ndk.abis.arch_to_triple(arch) |
| arch_name = "arch-{}".format(arch) |
| lib_dir = "lib64" if arch == "x86_64" else "lib" |
| src_dir = platforms_dir / platform / arch_name / "usr" / lib_dir |
| dst_dir = install_dir / "sysroot/usr/lib" / triple / str(api) |
| shutil.copytree(src_dir, dst_dir, ignore=shutil.ignore_patterns("*.a")) |
| |
| write_clang_wrapper( |
| install_dir / "bin", api, triple, self.host.is_windows |
| ) |
| |
| # Clang searches for libstdc++ headers at $GCC_PATH/../include/c++. It |
| # maybe be worth adding a search for the same path within the usual |
| # sysroot location to centralize these, or possibly just remove them |
| # from the NDK since they aren't particularly useful anyway. |
| system_stl_hdr_dir = install_dir / "include/c++" |
| system_stl_hdr_dir.mkdir(parents=True) |
| system_stl_inc_src = system_stl_dir / "include" |
| system_stl_inc_dst = system_stl_hdr_dir / "4.9.x" |
| shutil.copytree(system_stl_inc_src, system_stl_inc_dst) |
| |
| |
| @register |
| class Vulkan(ndk.builds.Module): |
| name = "vulkan" |
| install_path = Path("sources/third_party/vulkan") |
| notice = ANDROID_DIR / "external/vulkan-headers/NOTICE" |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| default_ignore_patterns = shutil.ignore_patterns( |
| "*CMakeLists.txt", "*test.cc", "linux", "windows" |
| ) |
| |
| source_dir = ANDROID_DIR / "external/vulkan-headers" |
| dest_dir = self.get_install_path() / "src" |
| for d in ["include", "registry"]: |
| src = source_dir / d |
| dst = dest_dir / d |
| shutil.rmtree(dst, ignore_errors=True) |
| shutil.copytree(src, dst, ignore=default_ignore_patterns) |
| |
| android_mk = dest_dir / "build-android/jni/Android.mk" |
| android_mk.parent.mkdir(parents=True, exist_ok=True) |
| url = "https://github.com/KhronosGroup/Vulkan-ValidationLayers" |
| android_mk.write_text( |
| textwrap.dedent( |
| f"""\ |
| $(warning The Vulkan Validation Layers are now distributed on \\ |
| GitHub. See {url} for more information.) |
| """ |
| ) |
| ) |
| |
| |
| @register |
| class Toolchain(ndk.builds.Module): |
| """The complete toolchain. |
| |
| BaseToolchain installs the core of the toolchain. This module installs the |
| STL to that toolchain. |
| """ |
| |
| name = "toolchain" |
| # This is installed to the Clang location to avoid migration pain. |
| install_path = Path("toolchains/llvm/prebuilt/{host}") |
| notice_group = ndk.builds.NoticeGroup.TOOLCHAIN |
| deps = { |
| "base-toolchain", |
| "libc++", |
| "libc++abi", |
| "platforms", |
| } |
| |
| @property |
| def notices(self) -> Iterator[Path]: |
| yield from Libcxx().notices |
| yield from Libcxxabi().notices |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| install_dir = self.get_install_path() |
| libcxx_dir = self.get_dep("libc++").get_install_path() |
| libcxxabi_dir = self.get_dep("libc++abi").get_install_path() |
| |
| libcxx_hdr_dir = install_dir / "sysroot/usr/include/c++" |
| libcxx_hdr_dir.mkdir(parents=True) |
| libcxx_inc_src = libcxx_dir / "include" |
| libcxx_inc_dst = libcxx_hdr_dir / "v1" |
| shutil.copytree(libcxx_inc_src, libcxx_inc_dst) |
| |
| libcxxabi_inc_src = libcxxabi_dir / "include" |
| shutil.copytree(libcxxabi_inc_src, libcxx_inc_dst, dirs_exist_ok=True) |
| |
| for arch in ndk.abis.ALL_ARCHITECTURES: |
| triple = ndk.abis.arch_to_triple(arch) |
| (abi,) = ndk.abis.arch_to_abis(arch) |
| sysroot_dst = install_dir / "sysroot/usr/lib" / triple |
| |
| shutil.copy2( |
| self.out_dir / "libcxx" / "libs" / abi / "libc++_shared.so", sysroot_dst |
| ) |
| static_libs = [ |
| "libc++_static.a", |
| "libc++abi.a", |
| ] |
| |
| for lib in static_libs: |
| shutil.copy2( |
| self.out_dir / "libcxx" / "obj" / "local" / abi / lib, sysroot_dst |
| ) |
| |
| platforms = self.get_dep("platforms") |
| assert isinstance(platforms, Platforms) |
| # Also install a libc++.so and libc++.a linker script per API level. |
| for api in platforms.get_apis(): |
| if api in Platforms.skip_apis: |
| continue |
| |
| for arch in platforms.get_arches(api): |
| triple = ndk.abis.arch_to_triple(arch) |
| dst_dir = install_dir / "sysroot/usr/lib" / triple / str(api) |
| |
| static_script = ["-lc++_static", "-lc++abi"] |
| shared_script = ["-lc++_shared"] |
| |
| libcxx_so_path = dst_dir / "libc++.so" |
| with open(libcxx_so_path, "w") as script: |
| script.write("INPUT({})".format(" ".join(shared_script))) |
| |
| libcxx_a_path = dst_dir / "libc++.a" |
| with open(libcxx_a_path, "w") as script: |
| script.write("INPUT({})".format(" ".join(static_script))) |
| |
| |
| def make_format_value(value: Any) -> Any: |
| if isinstance(value, list): |
| return " ".join(value) |
| return value |
| |
| |
| def var_dict_to_make(var_dict: Dict[str, Any]) -> str: |
| lines = [] |
| for name, value in var_dict.items(): |
| lines.append("{} := {}".format(name, make_format_value(value))) |
| return os.linesep.join(lines) |
| |
| |
| def cmake_format_value(value: Any) -> Any: |
| if isinstance(value, list): |
| return ";".join(value) |
| return value |
| |
| |
| def var_dict_to_cmake(var_dict: Dict[str, Any]) -> str: |
| lines = [] |
| for name, value in var_dict.items(): |
| lines.append('set({} "{}")'.format(name, cmake_format_value(value))) |
| return os.linesep.join(lines) |
| |
| |
| def abis_meta_transform(metadata: dict[str, Any]) -> dict[str, Any]: |
| default_abis = [] |
| deprecated_abis = [] |
| lp32_abis = [] |
| lp64_abis = [] |
| abi_infos = {} |
| for abi, abi_data in metadata.items(): |
| bitness = abi_data["bitness"] |
| if bitness == 32: |
| lp32_abis.append(abi) |
| elif bitness == 64: |
| lp64_abis.append(abi) |
| else: |
| raise ValueError("{} bitness is unsupported value: {}".format(abi, bitness)) |
| |
| if abi_data["default"]: |
| default_abis.append(abi) |
| |
| if abi_data["deprecated"]: |
| deprecated_abis.append(abi) |
| |
| proc = abi_data["proc"] |
| arch = abi_data["arch"] |
| triple = abi_data["triple"] |
| llvm_triple = abi_data["llvm_triple"] |
| abi_infos[f"NDK_ABI_{abi}_PROC"] = proc |
| abi_infos[f"NDK_ABI_{abi}_ARCH"] = arch |
| abi_infos[f"NDK_ABI_{abi}_TRIPLE"] = triple |
| abi_infos[f"NDK_ABI_{abi}_LLVM_TRIPLE"] = llvm_triple |
| abi_infos[f"NDK_PROC_{proc}_ABI"] = abi |
| abi_infos[f"NDK_ARCH_{arch}_ABI"] = abi |
| |
| meta_vars = { |
| "NDK_DEFAULT_ABIS": sorted(default_abis), |
| "NDK_DEPRECATED_ABIS": sorted(deprecated_abis), |
| "NDK_KNOWN_DEVICE_ABI32S": sorted(lp32_abis), |
| "NDK_KNOWN_DEVICE_ABI64S": sorted(lp64_abis), |
| "NDK_KNOWN_DEVICE_ABIS": sorted(lp32_abis + lp64_abis), |
| } |
| meta_vars.update(abi_infos) |
| |
| return meta_vars |
| |
| |
| def platforms_meta_transform(metadata: dict[str, Any]) -> dict[str, Any]: |
| meta_vars = { |
| "NDK_MIN_PLATFORM_LEVEL": metadata["min"], |
| "NDK_MAX_PLATFORM_LEVEL": metadata["max"], |
| } |
| |
| for src, dst in metadata["aliases"].items(): |
| name = "NDK_PLATFORM_ALIAS_{}".format(src) |
| value = "android-{}".format(dst) |
| meta_vars[name] = value |
| return meta_vars |
| |
| |
| def system_libs_meta_transform(metadata: dict[str, Any]) -> dict[str, Any]: |
| # This file also contains information about the first supported API level |
| # for each library. We could use this to provide better diagnostics in |
| # ndk-build, but currently do not. |
| return {"NDK_SYSTEM_LIBS": sorted(metadata.keys())} |
| |
| |
| @register |
| class NdkBuild(ndk.builds.PackageModule): |
| name = "ndk-build" |
| install_path = Path("build") |
| src = NDK_DIR / "build" |
| notice = NDK_DIR / "NOTICE" |
| |
| deps = { |
| "meta", |
| "clang", |
| } |
| |
| def install(self) -> None: |
| super().install() |
| |
| self.install_ndk_version_makefile() |
| self.generate_cmake_compiler_id() |
| |
| self.generate_language_specific_metadata("abis", abis_meta_transform) |
| |
| self.generate_language_specific_metadata("platforms", platforms_meta_transform) |
| |
| self.generate_language_specific_metadata( |
| "system_libs", system_libs_meta_transform |
| ) |
| |
| def install_ndk_version_makefile(self) -> None: |
| """Generates a version.mk for ndk-build.""" |
| version_mk = Path(self.get_install_path()) / "core/version.mk" |
| version_mk.write_text( |
| textwrap.dedent( |
| f"""\ |
| NDK_MAJOR := {ndk.config.major} |
| NDK_MINOR := {ndk.config.hotfix} |
| NDK_BETA := {ndk.config.beta} |
| NDK_CANARY := {str(ndk.config.canary).lower()} |
| """ |
| ) |
| ) |
| |
| @staticmethod |
| def get_clang_version(clang: Path) -> str: |
| """Invokes Clang to determine its version string.""" |
| result = subprocess.run( |
| [str(clang), "--version"], capture_output=True, encoding="utf-8", check=True |
| ) |
| version_line = result.stdout.splitlines()[0] |
| # Format of the version line is: |
| # Android ($BUILD, based on $REV) clang version x.y.z ($GIT_URL $SHA) |
| match = re.search(r"clang version ([0-9.]+)\s", version_line) |
| if match is None: |
| raise RuntimeError(f"Could not find Clang version in:\n{result.stdout}") |
| return match.group(1) |
| |
| def generate_cmake_compiler_id(self) -> None: |
| """Generates compiler ID information for old versions of CMake.""" |
| compiler_id_file = Path(self.get_install_path()) / "cmake/compiler_id.cmake" |
| clang_prebuilts = Path(self.get_dep("clang").get_build_host_install()) |
| clang = clang_prebuilts / "bin/clang" |
| clang_version = self.get_clang_version(clang) |
| |
| compiler_id_file.write_text( |
| textwrap.dedent( |
| f"""\ |
| # The file is automatically generated when the NDK is built. |
| set(CMAKE_ASM_COMPILER_VERSION {clang_version}) |
| set(CMAKE_C_COMPILER_VERSION {clang_version}) |
| set(CMAKE_CXX_COMPILER_VERSION {clang_version}) |
| """ |
| ) |
| ) |
| |
| def generate_language_specific_metadata( |
| self, name: str, func: Callable[[dict[str, Any]], dict[str, Any]] |
| ) -> None: |
| install_path = self.get_install_path() |
| json_path = self.get_dep("meta").get_install_path() / (name + ".json") |
| meta = json.loads(ndk.file.read_file(json_path)) |
| meta_vars = func(meta) |
| |
| ndk.file.write_file( |
| install_path / "core/{}.mk".format(name), |
| var_dict_to_make(meta_vars), |
| ) |
| ndk.file.write_file( |
| install_path / "cmake/{}.cmake".format(name), |
| var_dict_to_cmake(meta_vars), |
| ) |
| |
| |
| @register |
| class PythonPackages(ndk.builds.PackageModule): |
| name = "python-packages" |
| install_path = Path("python-packages") |
| src = ANDROID_DIR / "development/python-packages" |
| |
| |
| @register |
| class SystemStl(ndk.builds.PackageModule): |
| name = "system-stl" |
| install_path = Path("sources/cxx-stl/system") |
| src = NDK_DIR / "sources/cxx-stl/system" |
| |
| |
| @register |
| class Libcxxabi(ndk.builds.PackageModule): |
| name = "libc++abi" |
| install_path = Path("sources/cxx-stl/llvm-libc++abi") |
| src = ANDROID_DIR / "toolchain/llvm-project/libcxxabi" |
| |
| |
| @register |
| class SimplePerf(ndk.builds.Module): |
| name = "simpleperf" |
| install_path = Path("simpleperf") |
| notice = ANDROID_DIR / "prebuilts/simpleperf/NOTICE" |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| print("Installing simpleperf...") |
| install_dir = self.get_install_path() |
| if install_dir.exists(): |
| shutil.rmtree(install_dir) |
| install_dir.mkdir(parents=True) |
| |
| simpleperf_path = ndk.paths.android_path("prebuilts/simpleperf") |
| dirs = [ |
| Path("app_api"), |
| Path("bin/android"), |
| Path("doc"), |
| Path("inferno"), |
| Path("proto"), |
| Path("purgatorio"), |
| ] |
| host_bin_dir = "windows" if self.host.is_windows else self.host.value |
| dirs.append(Path("bin") / host_bin_dir) |
| for d in dirs: |
| shutil.copytree(simpleperf_path / d, install_dir / d) |
| |
| for item in os.listdir(simpleperf_path): |
| should_copy = False |
| if item.endswith(".py") and item != "update.py": |
| should_copy = True |
| elif item == "report_html.js": |
| should_copy = True |
| elif item == "inferno.sh" and not self.host.is_windows: |
| should_copy = True |
| elif item == "inferno.bat" and self.host.is_windows: |
| should_copy = True |
| if should_copy: |
| shutil.copy2(simpleperf_path / item, install_dir) |
| |
| shutil.copy2(simpleperf_path / "ChangeLog", install_dir) |
| |
| |
| @register |
| class Changelog(ndk.builds.FileModule): |
| name = "changelog" |
| install_path = Path("CHANGELOG.md") |
| src = NDK_DIR / f"docs/changelogs/Changelog-r{ndk.config.major}.md" |
| no_notice = True |
| |
| |
| @register |
| class NdkGdb(ndk.builds.MultiFileModule): |
| name = "ndk-gdb" |
| install_path = Path("prebuilt/{host}/bin") |
| notice = NDK_DIR / "NOTICE" |
| |
| @property |
| def files(self) -> Iterator[Path]: |
| yield NDK_DIR / "ndk-gdb" |
| yield NDK_DIR / "ndk-gdb.py" |
| |
| if self.host.is_windows: |
| yield NDK_DIR / "ndk-gdb.cmd" |
| |
| |
| @register |
| class NdkGdbShortcut(ndk.builds.ScriptShortcutModule): |
| name = "ndk-gdb-shortcut" |
| install_path = Path("ndk-gdb") |
| script = Path("prebuilt/{host}/bin/ndk-gdb") |
| windows_ext = ".cmd" |
| |
| |
| @register |
| class NdkLldbShortcut(ndk.builds.ScriptShortcutModule): |
| name = "ndk-lldb-shortcut" |
| install_path = Path("ndk-lldb") |
| script = Path("prebuilt/{host}/bin/ndk-gdb") |
| windows_ext = ".cmd" |
| |
| |
| @register |
| class NdkStack(ndk.builds.MultiFileModule): |
| name = "ndk-stack" |
| install_path = Path("prebuilt/{host}/bin") |
| notice = NDK_DIR / "NOTICE" |
| |
| @property |
| def files(self) -> Iterator[Path]: |
| yield NDK_DIR / "ndk-stack" |
| yield NDK_DIR / "ndk-stack.py" |
| |
| if self.host.is_windows: |
| yield NDK_DIR / "ndk-stack.cmd" |
| |
| |
| @register |
| class NdkStackShortcut(ndk.builds.ScriptShortcutModule): |
| name = "ndk-stack-shortcut" |
| install_path = Path("ndk-stack") |
| script = Path("prebuilt/{host}/bin/ndk-stack") |
| windows_ext = ".cmd" |
| |
| |
| @register |
| class NdkWhichShortcut(ndk.builds.ScriptShortcutModule): |
| name = "ndk-which-shortcut" |
| install_path = Path("ndk-which") |
| script = Path("prebuilt/{host}/bin/ndk-which") |
| windows_ext = "" # There isn't really a Windows ndk-which. |
| |
| |
| @register |
| class NdkBuildShortcut(ndk.builds.ScriptShortcutModule): |
| name = "ndk-build-shortcut" |
| install_path = Path("ndk-build") |
| script = Path("build/ndk-build") |
| windows_ext = ".cmd" |
| |
| |
| @register |
| class Readme(ndk.builds.FileModule): |
| name = "readme" |
| install_path = Path("README.md") |
| src = NDK_DIR / "UserReadme.md" |
| |
| |
| CANARY_TEXT = textwrap.dedent( |
| """\ |
| This is a canary build of the Android NDK. It's updated almost every day. |
| |
| Canary builds are designed for early adopters and can be prone to breakage. |
| Sometimes they can break completely. To aid development and testing, this |
| distribution can be installed side-by-side with your existing, stable NDK |
| release. |
| """ |
| ) |
| |
| |
| @register |
| class CanaryReadme(ndk.builds.Module): |
| name = "canary-readme" |
| install_path = Path("README.canary") |
| no_notice = True |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| if ndk.config.canary: |
| self.get_install_path().write_text(CANARY_TEXT) |
| |
| |
| @register |
| class Meta(ndk.builds.PackageModule): |
| name = "meta" |
| install_path = Path("meta") |
| src = NDK_DIR / "meta" |
| no_notice = True |
| |
| deps = { |
| "base-toolchain", |
| } |
| |
| def install(self) -> None: |
| super().install() |
| self.create_system_libs_meta() |
| |
| def create_system_libs_meta(self) -> None: |
| # Build system_libs.json based on what we find in the toolchain. We |
| # only need to scan a single 32-bit architecture since these libraries |
| # do not vary in availability across architectures. |
| sysroot_base = ( |
| self.get_dep("base-toolchain").get_install_path() |
| / "sysroot/usr/lib/arm-linux-androideabi" |
| ) |
| |
| system_libs: Dict[str, str] = {} |
| for api_name in sorted(os.listdir(sysroot_base)): |
| path = sysroot_base / api_name |
| |
| # There are also non-versioned libraries in this directory. |
| if not path.is_dir(): |
| continue |
| |
| for lib in os.listdir(path): |
| # Don't include CRT objects in the list. |
| if not lib.endswith(".so"): |
| continue |
| |
| if not lib.startswith("lib"): |
| raise RuntimeError( |
| "Found unexpected file in sysroot: {}".format(lib) |
| ) |
| |
| # libc++.so is a linker script, not a system library. |
| if lib == "libc++.so": |
| continue |
| |
| # We're processing each version directory in sorted order, so |
| # if we've already seen this library before it is an earlier |
| # version of the library. |
| if lib in system_libs: |
| continue |
| |
| system_libs[lib] = api_name |
| |
| system_libs = collections.OrderedDict(sorted(system_libs.items())) |
| |
| json_path = self.get_install_path() / "system_libs.json" |
| with open(json_path, "w") as json_file: |
| json.dump(system_libs, json_file, indent=2, separators=(",", ": ")) |
| |
| |
| @register |
| class WrapSh(ndk.builds.PackageModule): |
| name = "wrap.sh" |
| install_path = Path("wrap.sh") |
| src = NDK_DIR / "wrap.sh" |
| no_notice = True |
| |
| |
| @register |
| class SourceProperties(ndk.builds.Module): |
| name = "source.properties" |
| install_path = Path("source.properties") |
| no_notice = True |
| |
| def build(self) -> None: |
| pass |
| |
| def install(self) -> None: |
| path = self.get_install_path() |
| with open(path, "w") as source_properties: |
| assert self.context is not None |
| version = get_version_string(self.context.build_number) |
| if ndk.config.beta > 0: |
| version += "-beta{}".format(ndk.config.beta) |
| source_properties.writelines( |
| ["Pkg.Desc = Android NDK\n", "Pkg.Revision = {}\n".format(version)] |
| ) |
| |
| |
| def create_notice_file(path: Path, for_group: ndk.builds.NoticeGroup) -> None: |
| # Using sets here so we can perform some amount of duplicate reduction. In |
| # a lot of cases there will be minor differences that cause lots of |
| # "duplicates", but might as well catch what we can. |
| notice_files = set() |
| for module in ALL_MODULES: |
| if module.notice_group == for_group: |
| for notice in module.notices: |
| notice_files.add(notice) |
| |
| licenses = set() |
| for notice_path in notice_files: |
| with open(notice_path, encoding="utf-8") as notice_file: |
| licenses.add(notice_file.read()) |
| |
| with path.open("w", encoding="utf-8") as output_file: |
| # Sorting the contents here to try to make things deterministic. |
| output_file.write(os.linesep.join(sorted(list(licenses)))) |
| |
| |
| def launch_build( |
| worker: ndk.workqueue.Worker, |
| module: ndk.builds.Module, |
| log_dir: Path, |
| debuggable: bool, |
| ) -> Tuple[bool, ndk.builds.Module]: |
| result = do_build(worker, module, log_dir, debuggable) |
| if not result: |
| return result, module |
| do_install(worker, module) |
| return True, module |
| |
| |
| @contextlib.contextmanager |
| def file_logged_context(path: Path) -> Iterator[None]: |
| with path.open("w") as log_file: |
| os.dup2(log_file.fileno(), sys.stdout.fileno()) |
| os.dup2(log_file.fileno(), sys.stderr.fileno()) |
| yield |
| |
| |
| def do_build( |
| worker: ndk.workqueue.Worker, |
| module: ndk.builds.Module, |
| log_dir: Path, |
| debuggable: bool, |
| ) -> bool: |
| if debuggable: |
| cm: ContextManager[None] = contextlib.nullcontext() |
| else: |
| cm = file_logged_context(module.log_path(log_dir)) |
| with cm: |
| try: |
| worker.status = f"Building {module}..." |
| module.build() |
| return True |
| except Exception: # pylint: disable=broad-except |
| traceback.print_exc() |
| return False |
| |
| |
| def do_install(worker: ndk.workqueue.Worker, module: ndk.builds.Module) -> None: |
| worker.status = "Installing {}...".format(module) |
| module.install() |
| |
| |
| def _get_transitive_module_deps( |
| module: ndk.builds.Module, |
| deps: Set[ndk.builds.Module], |
| unknown_deps: Set[str], |
| seen: Set[ndk.builds.Module], |
| ) -> None: |
| seen.add(module) |
| |
| for name in module.deps: |
| if name not in NAMES_TO_MODULES: |
| unknown_deps.add(name) |
| continue |
| |
| dep = NAMES_TO_MODULES[name] |
| if dep in seen: |
| # Cycle detection is already handled by ndk.deps.DependencyManager. |
| # Just avoid falling into an infinite loop here and let that do the |
| # work. |
| continue |
| |
| deps.add(dep) |
| _get_transitive_module_deps(dep, deps, unknown_deps, seen) |
| |
| |
| def get_transitive_module_deps( |
| module: ndk.builds.Module, |
| ) -> Tuple[Set[ndk.builds.Module], Set[str]]: |
| seen: Set[ndk.builds.Module] = set() |
| deps: Set[ndk.builds.Module] = set() |
| unknown_deps: Set[str] = set() |
| _get_transitive_module_deps(module, deps, unknown_deps, seen) |
| return deps, unknown_deps |
| |
| |
| def get_modules_to_build( |
| module_names: Iterable[str], |
| ) -> Tuple[List[ndk.builds.Module], Set[ndk.builds.Module]]: |
| """Returns a list of modules to be built given a list of module names. |
| |
| The module names are those given explicitly by the user or the full list. |
| In the event that the user has passed a subset of modules, we need to also |
| return the dependencies of that module. |
| """ |
| unknown_modules = set() |
| modules = set() |
| deps_only = set() |
| for name in module_names: |
| if name not in NAMES_TO_MODULES: |
| # Build a list of all the unknown modules rather than error out |
| # immediately so we can provide a complete error message. |
| unknown_modules.add(name) |
| |
| module = NAMES_TO_MODULES[name] |
| modules.add(module) |
| |
| deps, unknown_deps = get_transitive_module_deps(module) |
| modules.update(deps) |
| |
| # --skip-deps may be passed if the user wants to avoid rebuilding a |
| # costly dependency. It's up to the user to guarantee that the |
| # dependency has actually been built. Modules are skipped by |
| # immediately completing them rather than sending them to the |
| # workqueue. As such, we need to return a list of which modules are |
| # *only* in the list because they are dependencies rather than being a |
| # part of the requested set. |
| for dep in deps: |
| if dep.name not in module_names: |
| deps_only.add(dep) |
| unknown_modules.update(unknown_deps) |
| |
| if unknown_modules: |
| sys.exit("Unknown modules: {}".format(", ".join(sorted(list(unknown_modules))))) |
| |
| build_modules = [] |
| for module in modules: |
| build_modules.append(module) |
| |
| return sorted(list(build_modules), key=str), deps_only |
| |
| |
| ALL_MODULES = [t() for t in ALL_MODULE_TYPES] |
| NAMES_TO_MODULES = {m.name: m for m in ALL_MODULES} |
| |
| |
| def get_all_module_names() -> List[str]: |
| return [m.name for m in ALL_MODULES if m.enabled] |
| |
| |
| def build_number_arg(value: str) -> str: |
| if value.startswith("P"): |
| # Treehugger build. Treat as a local development build. |
| return "0" |
| return value |
| |
| |
| def parse_args() -> Tuple[argparse.Namespace, List[str]]: |
| parser = argparse.ArgumentParser(description=inspect.getdoc(sys.modules[__name__])) |
| |
| parser.add_argument( |
| "-v", |
| "--verbose", |
| action="count", |
| dest="verbosity", |
| default=0, |
| help="Increase logging verbosity.", |
| ) |
| |
| parser.add_argument( |
| "--permissive-python-environment", |
| action="store_true", |
| help=( |
| "Disable strict Python path checking. This allows using a non-prebuilt " |
| "Python when one is not available." |
| ), |
| ) |
| |
| parser.add_argument( |
| "-j", |
| "--jobs", |
| type=int, |
| default=multiprocessing.cpu_count(), |
| help=( |
| "Number of parallel builds to run. Note that this will not " |
| "affect the -j used for make; this just parallelizes " |
| "checkbuild.py. Defaults to the number of CPUs available. " |
| "Disabled when --debugabble is used." |
| ), |
| ) |
| |
| parser.add_argument( |
| "--debuggable", |
| action="store_true", |
| help=( |
| "Prints build output to the console and disables threading to " |
| "allow debugging with breakpoint()" |
| ), |
| ) |
| |
| parser.add_argument( |
| "--skip-deps", |
| action="store_true", |
| help=( |
| "Assume that dependencies have been built and only build " |
| "explicitly named modules." |
| ), |
| ) |
| |
| package_group = parser.add_mutually_exclusive_group() |
| package_group.add_argument( |
| "--package", |
| action="store_true", |
| dest="package", |
| help="Package the NDK when done building.", |
| ) |
| package_group.add_argument( |
| "--no-package", |
| action="store_false", |
| dest="package", |
| help="Do not package the NDK when done building (default).", |
| ) |
| |
| test_group = parser.add_mutually_exclusive_group() |
| test_group.add_argument( |
| "--build-tests", |
| action="store_true", |
| dest="build_tests", |
| default=True, |
| help="Build tests when finished. Not supported when targeting Windows.", |
| ) |
| test_group.add_argument( |
| "--no-build-tests", |
| action="store_false", |
| dest="build_tests", |
| help="Skip building tests after building the NDK.", |
| ) |
| |
| package_test_group = parser.add_mutually_exclusive_group() |
| package_test_group.add_argument( |
| "--package-tests", |
| action="store_true", |
| dest="package_tests", |
| default=None, |
| help="Package tests as build artifacts. Requires --build-tests.", |
| ) |
| package_test_group.add_argument( |
| "--no-package-tests", |
| action="store_false", |
| dest="package_tests", |
| default=None, |
| help="Don't package tests after building them.", |
| ) |
| |
| parser.add_argument( |
| "--build-number", |
| default="0", |
| type=build_number_arg, |
| help="Build number for use in version files.", |
| ) |
| parser.add_argument("--release", help="Ignored. Temporarily compatibility.") |
| |
| parser.add_argument( |
| "--system", |
| choices=Host, |
| type=Host, |
| default=Host.current(), |
| help="Build for the given OS.", |
| ) |
| |
| module_group = parser.add_mutually_exclusive_group() |
| |
| module_group.add_argument( |
| "--module", |
| dest="modules", |
| action="append", |
| default=[], |
| choices=get_all_module_names(), |
| help="NDK modules to build.", |
| ) |
| |
| return parser.parse_known_args() |
| |
| |
| def log_build_failure(log_path: Path, dist_dir: Path) -> None: |
| with open(log_path, "r") as log_file: |
| contents = log_file.read() |
| print(contents) |
| |
| # The build server has a build_error.log file that is supposed to be |
| # the short log of the failure that stopped the build. Append our |
| # failing log to that. |
| build_error_log = dist_dir / "logs/build_error.log" |
| with open(build_error_log, "a") as error_log: |
| error_log.write("\n") |
| error_log.write(contents) |
| |
| |
| def launch_buildable( |
| deps: ndk.deps.DependencyManager, |
| workqueue: ndk.workqueue.AnyWorkQueue, |
| log_dir: Path, |
| debuggable: bool, |
| skip_deps: bool, |
| skip_modules: Set[ndk.builds.Module], |
| ) -> None: |
| # If args.skip_deps is true, we could get into a case where we just |
| # dequeued the only module that was still building and the only |
| # items in get_buildable() are modules that will be skipped. |
| # Without this outer while loop, we'd mark the skipped dependencies |
| # as complete and then complete the outer loop. The workqueue |
| # would be out of work and we'd exit. |
| # |
| # Avoid this by making sure that we queue all possible buildable |
| # modules before we complete the loop. |
| while deps.buildable_modules: |
| for module in deps.get_buildable(): |
| if skip_deps and module in skip_modules: |
| deps.complete(module) |
| continue |
| workqueue.add_task(launch_build, module, log_dir, debuggable) |
| |
| |
| @contextlib.contextmanager |
| def build_ui_context(debuggable: bool) -> Iterator[None]: |
| if debuggable: |
| yield |
| else: |
| console = ndk.ansi.get_console() |
| with ndk.ansi.disable_terminal_echo(sys.stdin): |
| with console.cursor_hide_context(): |
| yield |
| |
| |
| def wait_for_build( |
| deps: ndk.deps.DependencyManager, |
| workqueue: ndk.workqueue.AnyWorkQueue, |
| dist_dir: Path, |
| log_dir: Path, |
| debuggable: bool, |
| skip_deps: bool, |
| skip_modules: Set[ndk.builds.Module], |
| ) -> None: |
| console = ndk.ansi.get_console() |
| ui = ndk.ui.get_build_progress_ui(console, workqueue) |
| with build_ui_context(debuggable): |
| while not workqueue.finished(): |
| result, module = workqueue.get_result() |
| if not result: |
| ui.clear() |
| print("Build failed: {}".format(module)) |
| log_build_failure(module.log_path(log_dir), dist_dir) |
| sys.exit(1) |
| elif not console.smart_console: |
| ui.clear() |
| print("Build succeeded: {}".format(module)) |
| |
| deps.complete(module) |
| launch_buildable( |
| deps, workqueue, log_dir, debuggable, skip_deps, skip_modules |
| ) |
| |
| ui.draw() |
| ui.clear() |
| print("Build finished") |
| |
| |
| def check_ndk_symlink(ndk_dir: Path, src: Path, target: Path) -> None: |
| """Check that the symlink's target is relative, exists, and points within |
| the NDK installation. |
| """ |
| if target.is_absolute(): |
| raise RuntimeError(f"Symlink {src} points to absolute path {target}") |
| ndk_dir = ndk_dir.resolve() |
| cur = src.parent.resolve() |
| for part in target.parts: |
| # (cur / part) might itself be a symlink. Its validity is checked from |
| # the top-level scan, so it doesn't need to be checked here. |
| cur = (cur / part).resolve() |
| if not cur.exists(): |
| raise RuntimeError(f"Symlink {src} targets non-existent {cur}") |
| if not cur.is_relative_to(ndk_dir): |
| raise RuntimeError(f"Symlink {src} targets {cur} outside NDK {ndk_dir}") |
| |
| |
| def check_ndk_symlinks(ndk_dir: Path, host: Host) -> None: |
| for path in ndk.paths.walk(ndk_dir): |
| if not path.is_symlink(): |
| continue |
| if host == Host.Windows64: |
| # Symlinks aren't supported well enough on Windows. (e.g. They |
| # require Developer Mode and/or special permissions. Cygwin |
| # tools might create symlinks that non-Cygwin programs don't |
| # recognize.) |
| raise RuntimeError(f"Symlink {path} unexpected in Windows NDK") |
| check_ndk_symlink(ndk_dir, path, path.readlink()) |
| |
| |
| def build_ndk( |
| modules: List[ndk.builds.Module], |
| deps_only: Set[ndk.builds.Module], |
| out_dir: Path, |
| dist_dir: Path, |
| args: argparse.Namespace, |
| ) -> Path: |
| build_context = ndk.builds.BuildContext( |
| out_dir, dist_dir, ALL_MODULES, args.system, args.build_number |
| ) |
| |
| for module in modules: |
| module.context = build_context |
| |
| log_dir = dist_dir / "logs" |
| log_dir.mkdir(parents=True, exist_ok=True) |
| |
| ndk_dir = ndk.paths.get_install_path(out_dir, args.system) |
| ndk_dir.mkdir(parents=True, exist_ok=True) |
| |
| deps = ndk.deps.DependencyManager(modules) |
| if args.debuggable: |
| workqueue: ndk.workqueue.AnyWorkQueue = ndk.workqueue.BasicWorkQueue() |
| else: |
| workqueue = ndk.workqueue.WorkQueue(args.jobs) |
| try: |
| launch_buildable( |
| deps, workqueue, log_dir, args.debuggable, args.skip_deps, deps_only |
| ) |
| wait_for_build( |
| deps, |
| workqueue, |
| dist_dir, |
| log_dir, |
| args.debuggable, |
| args.skip_deps, |
| deps_only, |
| ) |
| |
| if deps.get_buildable(): |
| raise RuntimeError( |
| "Builder stopped early. Modules are still " |
| "buildable: {}".format(", ".join(str(deps.get_buildable()))) |
| ) |
| |
| create_notice_file(ndk_dir / "NOTICE", ndk.builds.NoticeGroup.BASE) |
| create_notice_file( |
| ndk_dir / "NOTICE.toolchain", ndk.builds.NoticeGroup.TOOLCHAIN |
| ) |
| check_ndk_symlinks(ndk_dir, args.system) |
| return ndk_dir |
| finally: |
| workqueue.terminate() |
| workqueue.join() |
| |
| |
| def build_ndk_for_cross_compile(out_dir: Path, args: argparse.Namespace) -> None: |
| args = copy.deepcopy(args) |
| args.system = Host.current() |
| if args.system != Host.Linux: |
| raise NotImplementedError |
| module_names = NAMES_TO_MODULES.keys() |
| modules, deps_only = get_modules_to_build(module_names) |
| print("Building Linux modules: {}".format(" ".join([str(m) for m in modules]))) |
| build_ndk(modules, deps_only, out_dir, out_dir, args) |
| |
| |
| def create_ndk_symlink(out_dir: Path) -> None: |
| this_host_ndk = ndk.paths.get_install_path() |
| ndk_symlink = out_dir / this_host_ndk.name |
| if not ndk_symlink.exists(): |
| os.symlink(this_host_ndk, ndk_symlink) |
| |
| |
| def get_directory_size(path: Path) -> int: |
| du_str = subprocess.check_output(["du", "-sm", str(path)]) |
| match = re.match(r"^(\d+)", du_str.decode("utf-8")) |
| if match is None: |
| raise RuntimeError(f"Could not determine the size of {path}") |
| size_str = match.group(1) |
| return int(size_str) |
| |
| |
| def main() -> None: |
| total_timer = ndk.timer.Timer() |
| total_timer.start() |
| |
| args, module_names = parse_args() |
| |
| ensure_python_environment(args.permissive_python_environment) |
| |
| if args.verbosity >= 2: |
| logging.basicConfig(level=logging.DEBUG) |
| elif args.verbosity == 1: |
| logging.basicConfig(level=logging.INFO) |
| else: |
| logging.basicConfig() |
| |
| module_names.extend(args.modules) |
| if not module_names: |
| module_names = get_all_module_names() |
| |
| required_package_modules = set(get_all_module_names()) |
| have_required_modules = required_package_modules <= set(module_names) |
| |
| if args.package_tests is None: |
| args.package_tests = args.package |
| |
| # TODO(danalbert): wine? |
| # We're building the Windows packages from Linux, so we can't actually run |
| # any of the tests from here. |
| if args.system.is_windows or not (have_required_modules or args.skip_deps): |
| args.build_tests = False |
| |
| os.chdir(Path(__file__).resolve().parent.parent) |
| |
| # Set ANDROID_BUILD_TOP. |
| if "ANDROID_BUILD_TOP" in os.environ: |
| sys.exit( |
| textwrap.dedent( |
| """\ |
| Error: ANDROID_BUILD_TOP is already set in your environment. |
| |
| This typically means you are running in a shell that has launched a |
| target in a platform build. The platform environment interferes |
| with the NDK build environment, so the build cannot continue. |
| |
| Launch a new shell before building the NDK.""" |
| ) |
| ) |
| |
| os.environ["ANDROID_BUILD_TOP"] = str(ndk.paths.android_path()) |
| |
| out_dir = ndk.paths.get_out_dir() |
| dist_dir = ndk.paths.get_dist_dir() |
| |
| print("Machine has {} CPUs".format(multiprocessing.cpu_count())) |
| |
| if args.system.is_windows and not args.skip_deps: |
| # Since the Windows NDK is cross compiled, we need to build a Linux NDK |
| # first so we can build components like libc++. |
| build_ndk_for_cross_compile(Path(out_dir), args) |
| |
| modules, deps_only = get_modules_to_build(module_names) |
| print( |
| "Building modules: {}".format( |
| " ".join( |
| [str(m) for m in modules if not args.skip_deps or m not in deps_only] |
| ) |
| ) |
| ) |
| |
| build_timer = ndk.timer.Timer() |
| with build_timer: |
| ndk_dir = build_ndk(modules, deps_only, out_dir, dist_dir, args) |
| installed_size = get_directory_size(ndk_dir) |
| |
| # Create a symlink to the NDK usable by this host in the root of the out |
| # directory for convenience. |
| create_ndk_symlink(out_dir) |
| |
| package_timer = ndk.timer.Timer() |
| with package_timer: |
| if args.package: |
| print("Packaging NDK...") |
| # NB: Purging of unwanted files (.pyc, Android.bp, etc) happens as |
| # part of packaging. If testing is ever moved to happen before |
| # packaging, ensure that the directory is purged before and after |
| # building the tests. |
| package_path = package_ndk( |
| ndk_dir, out_dir, dist_dir, args.system, args.build_number |
| ) |
| packaged_size_bytes = package_path.stat().st_size |
| packaged_size = packaged_size_bytes // (2**20) |
| |
| good = True |
| test_timer = ndk.timer.Timer() |
| with test_timer: |
| if args.build_tests: |
| print("Building tests...") |
| purge_unwanted_files(ndk_dir) |
| good = build_ndk_tests(out_dir, dist_dir, args) |
| print() # Blank line between test results and timing data. |
| |
| total_timer.finish() |
| |
| print("") |
| print("Installed size: {} MiB".format(installed_size)) |
| if args.package: |
| print("Package size: {} MiB".format(packaged_size)) |
| print("Finished {}".format("successfully" if good else "unsuccessfully")) |
| print("Build: {}".format(build_timer.duration)) |
| print("Packaging: {}".format(package_timer.duration)) |
| print("Testing: {}".format(test_timer.duration)) |
| print("Total: {}".format(total_timer.duration)) |
| |
| subject = "NDK Build {}!".format("Passed" if good else "Failed") |
| body = "Build finished in {}".format(total_timer.duration) |
| ndk.notify.toast(subject, body) |
| |
| sys.exit(not good) |
| |
| |
| @contextlib.contextmanager |
| def _assign_self_to_new_process_group(fd: TextIO) -> Iterator[None]: |
| # It seems the build servers run us in our own session, in which case we |
| # get EPERM from `setpgrp`. No need to call this in that case because we |
| # will already be the process group leader. |
| if os.getpid() == os.getsid(os.getpid()): |
| yield |
| return |
| |
| if ndk.ansi.is_self_in_tty_foreground_group(fd): |
| old_pgrp = os.tcgetpgrp(fd.fileno()) |
| os.tcsetpgrp(fd.fileno(), os.getpid()) |
| os.setpgrp() |
| try: |
| yield |
| finally: |
| os.tcsetpgrp(fd.fileno(), old_pgrp) |
| else: |
| os.setpgrp() |
| yield |
| |
| |
| def _run_main_in_new_process_group() -> None: |
| with _assign_self_to_new_process_group(sys.stdin): |
| main() |
| |
| |
| if __name__ == "__main__": |
| _run_main_in_new_process_group() |