使用 Ninja(实验性)集成自定义 C/C++ 构建系统

如果您不使用 CMake 或 ndk-build,但希望完全集成 Android Gradle 插件 (AGP) C/C++ 构建和 Android Studio,您可以通过编写一个 shell 脚本来创建自定义 C/C++ 构建系统,该脚本以 Ninja 构建文件格式写入构建信息。

Android Studio 和 AGP 已添加对自定义 C/C++ 构建系统的实验性支持。此功能自 Android Studio Dolphin | 2021.3.1 Canary 4 起可用。

概览

C/C++ 项目(尤其是那些面向多个平台的项目)的常见模式是从某种底层表示生成针对每个平台的项目。此模式的一个突出示例是 CMake。CMake 可以从保存在 CMakeLists.txt 文件中的单一底层表示为 Android、iOS 和其他平台生成项目。

虽然 AGP 直接支持 CMake,但还有其他项目生成器不受直接支持:

这些类型的项目生成器要么支持 Ninja 作为 C/C++ 构建的后端表示,要么可以进行调整以生成 Ninja 作为后端表示。

正确配置后,集成 C/C++ 项目系统生成器的 AGP 项目可让用户:

  • 从命令行和 Android Studio 构建。

  • 在 Android Studio 中编辑源代码,并获得完整的语言服务支持(例如,转到定义)。

  • 使用 Android Studio 调试器调试原生和混合进程。

如何修改您的构建以使用自定义 C/C++ 构建配置脚本

本节将逐步介绍如何从 AGP 使用自定义 C/C++ 构建配置脚本。

步骤 1:修改模块级 build.gradle 文件以引用配置脚本

要在 AGP 中启用 Ninja 支持,请在模块级 build.gradle 文件中配置 experimentalProperties

android {
  defaultConfig {
    externalNativeBuild {
      experimentalProperties["ninja.abiFilters"] = [ "x86", "arm64-v8a" ]
      experimentalProperties["ninja.path"] = "source-file-list.txt"
      experimentalProperties["ninja.configure"] = "configure-ninja"
      experimentalProperties["ninja.arguments"] = [
            "\${ndk.moduleMakeFile}",
            "--variant=\${ndk.variantName}",
            "--abi=Android-\${ndk.abi}",
            "--configuration-dir=\${ndk.configurationDir}",
            "--ndk-version=\${ndk.moduleNdkVersion}",
            "--min-sdk-version=\${ndk.minSdkVersion}"
       ]
     }
   }

AGP 对属性的解释如下:

  • ninja.abiFilters 是要构建的 ABI 列表。有效值包括:x86x86-64armeabi-v7aarm64-v8a

  • ninja.path 是 C/C++ 项目文件的路径。此文件的格式可以是您想要的任何格式。对此文件的更改将触发 Android Studio 中的 Gradle 同步提示。

  • ninja.configure 是一个脚本文件的路径,Gradle 在需要配置 C/C++ 项目时会执行此脚本。项目会在首次构建时、Android Studio 中的 Gradle 同步期间或配置脚本的某个输入发生更改时进行配置。

  • ninja.arguments 是将传递给由 ninja.configure 定义的脚本的参数列表。此列表中的元素可以引用一组宏,这些宏的值取决于 AGP 中的当前配置上下文:

    • ${ndk.moduleMakeFile}ninja.configure 文件的完整路径。因此,在此示例中,它将是 C:\path\to\configure-ninja.bat

    • ${ndk.variantName} 是当前正在构建的 AGP 变体的名称。例如,debug 或 release。

    • ${ndk.abi} 是当前正在构建的 AGP ABI 的名称。例如,x86arm64-v8a

    • ${ndk.buildRoot} 是一个由 AGP 生成的文件夹的名称,脚本将输出写入该文件夹。具体细节将在步骤 2:创建配置脚本中解释。

    • ${ndk.ndkVersion} 是要使用的 NDK 版本。这通常是传递给 build.gradle 文件中 android.ndkVersion 的值,如果未提供则为默认值。

    • ${ndk.minPlatform} 是 AGP 请求的最低目标 Android 平台。

  • ninja.targets 是应构建的特定 Ninja 目标的列表。

步骤 2:创建配置脚本

配置脚本(在前面的示例中为 configure-ninja.bat)的最低职责是生成一个 build.ninja 文件,该文件在使用 Ninja 构建时将编译并链接项目的所有原生输出。通常这些是 .o(对象)、.a(归档)和 .so(共享对象)文件。

配置脚本可以根据您的需求将 build.ninja 文件写入两个不同的位置。

  • 如果 AGP 可以选择位置,则配置脚本会将 build.ninja 写入 ${ndk.buildRoot} 宏中设置的位置。

  • 如果配置脚本需要选择 build.ninja 文件的位置,那么它也会在 ${ndk.buildRoot} 宏中设置的位置写入一个名为 build.ninja.txt 的文件。此文件包含配置脚本写入的 build.ninja 文件的完整路径。

build.ninja 文件的结构

通常,大多数能够准确表示 Android C/C++ 构建的结构都将起作用。AGP 和 Android Studio 所需的关键元素是:

  • C/C++ 源文件列表以及 Clang 编译它们所需的标志。

  • 输出库列表。这些通常是 .so(共享对象)文件,但也可以是 .a(归档)文件或可执行文件(无扩展名)。

如果您需要如何生成 build.ninja 文件的示例,可以查看使用 build.ninja 生成器时 CMake 的输出。

以下是一个最小 build.ninja 模板的示例。

rule COMPILE
   command = /path/to/ndk/clang -c $in -o $out {other flags}
rule LINK
   command = /path/to/ndk/clang $in -o $out {other flags}

build source.o : COMPILE source.cpp
build lib.so : LINK source.o

最佳实践

除了要求(源文件列表和输出库)之外,以下是一些推荐的最佳实践。

使用 phony 规则声明命名输出

在可能的情况下,建议 build.ninja 结构使用 phony 规则为构建输出提供人类可读的名称。例如,如果您有一个名为 c:/path/to/lib.so 的输出,您可以按如下方式为其指定一个人类可读的名称。

build curl: phony /path/to/lib.so

这样做的好处是,您可以在 build.gradle 文件中将此名称指定为构建目标。例如,

android {
  defaultConfig {
    externalNativeBuild {
      ...
      experimentalProperties["ninja.targets"] = [ "curl" ]

指定一个“all”目标

当您指定一个 all 目标时,如果 build.gradle 文件中未明确指定任何目标,这将是 AGP 构建的默认库集。

rule COMPILE
   command = /path/to/ndk/clang $in -o $out {other flags}
rule LINK
   command = /path/to/ndk/clang $in -o $out {other flags}

build foo.o : COMPILE foo.cpp
build bar.o : COMPILE bar.cpp
build libfoo.so : LINK foo.o
build libbar.so : LINK bar.o
build all: phony libfoo.so libbar.so

指定备用构建方法(可选)

一个更高级的用例是封装一个现有的非 Ninja 构建系统。在这种情况下,您仍然需要表示所有源文件及其标志以及输出库,以便 Android Studio 可以提供适当的语言服务功能,例如自动补全和转到定义。但是,您希望 AGP 在实际构建期间委托给底层构建系统。

为此,您可以使用带有特定扩展名 .passthrough 的 Ninja 构建输出。

举一个更具体的例子,假设您想封装一个 MSBuild。您的配置脚本将像往常一样生成 build.ninja,但它还会添加一个直通目标,该目标定义 AGP 将如何调用 MSBuild。

rule COMPILE
   command = /path/to/ndk/clang $in -o $out {other flags}
rule LINK
   command = /path/to/ndk/clang $in -o $out {other flags}

rule MBSUILD_CURL
  command = /path/to/msbuild {flags to build curl with MSBuild}

build source.o : COMPILE source.cpp
build lib.so : LINK source.o
build curl : phony lib.so
build curl.passthrough : MBSUILD_CURL

提供反馈

此功能处于实验阶段,因此非常感谢您的反馈。您可以通过以下渠道提供反馈:

  • 对于一般反馈,请在此 bug 中添加评论。

  • 要报告错误,请打开 Android Studio 并点击 Help > Submit Feedback。请务必提及“Custom C/C++ Build Systems”以帮助定位错误。

  • 如果您未安装 Android Studio,请使用此模板提交错误报告。