Integrar sistemas de compilação C/C++ personalizados usando o Ninja (experimental)

Se você não usa o CMake ou o ndk-build, mas quer a integração completa do build C/C++ do Plug-in do Android para Gradle (AGP, na sigla em inglês) com o Android Studio, é possível criar um sistema de compilação C/C++ personalizado gerando um script de shell que grave informações do build no formato de arquivo de build do Ninja (link em inglês).

Foi adicionado o suporte experimental aos sistemas de compilação C/C++ personalizados no Android Studio e no AGP. Esse recurso está disponível do Android Studio Dolphin | 2021.3.1 Canary 4 em diante.

Visão geral

Um padrão comum em projetos C/C++, especialmente aqueles voltados a várias plataformas, é gerar projetos para cada uma dessas plataformas usando alguma representação. Um exemplo notório desse padrão é o CMake (link em inglês). Ele pode gerar projetos para Android, iOS e outras plataformas com uma única representação, salva no arquivo CMakeLists.txt.

Embora o AGP ofereça suporte direto ao CMake, há outros geradores de projeto disponíveis que não têm suporte direto (links em inglês):

Esses tipos de geradores de projetos oferecem suporte ao Ninja como uma representação de back-end do build C/C++ ou podem ser adaptados para gerar o Ninja como uma representação de back-end.

Quando configurado corretamente, um projeto do AGP com um gerador de sistema de projetos C/C++ integrado permite que os usuários:

  • criem usando a linha de comando e o Android Studio;

  • editem origens com suporte total ao serviço de idiomas (por exemplo, acesso à definição) no Android Studio;

  • usem os depuradores do Android Studio para depurar processos nativos e mistos.

Como modificar seu build para usar um script de configuração do build C/C++ personalizado

Esta seção mostra as etapas para usar um script de configuração do build C/C++ personalizado no AGP.

Etapa 1: modificar o arquivo build.gradle no nível do módulo para referenciar um script de configuração

Para ativar o suporte ao Ninja no AGP, configure as experimentalProperties no arquivo build.gradle no nível do módulo:

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}"
       ]
     }
   }

As propriedades são interpretadas pelo AGP da seguinte forma:

  • ninja.abiFilters é uma lista de ABIs a serem criadas. Os valores válidos são: x86, x86-64, armeabi-v7a e arm64-v8a.

  • ninja.path é um caminho para um arquivo do projeto C/C++. O formato desse arquivo pode ser qual você quiser. Mudanças nesse arquivo vão acionar uma solicitação para sincronizar o Gradle no Android Studio.

  • ninja.configure é um caminho para um arquivo de script que será executado pelo Gradle quando for necessário configurar o projeto C/C++. Um projeto é configurado no primeiro build, durante uma sincronização do Gradle no Android Studio ou quando uma das entradas do script de configuração muda.

  • ninja.arguments é uma lista de argumentos que serão transmitidos para o script definido por ninja.configure. Os elementos dessa lista podem se referir a um conjunto de macros com valores que dependem do contexto de configuração atual no AGP:

    • ${ndk.moduleMakeFile} é o caminho completo para o arquivo ninja.configure. No exemplo, ele seria C:\path\to\configure-ninja.bat.

    • ${ndk.variantName} é o nome da variante atual do AGP que está sendo criada. Por exemplo, depuração ou lançamento.

    • ${ndk.abi} é o nome da ABI atual do AGP que está sendo criada. Por exemplo, x86 ou arm64-v8a.

    • ${ndk.buildRoot} é o nome de uma pasta, gerada pelo AGP, em que o script grava a saída. Os detalhes serão explicados na Etapa 2: criar o script de configuração.

    • ${ndk.ndkVersion} é a versão do NDK que será usada. Geralmente, esse é o valor transmitido para android.ndkVersion no arquivo build.gradle ou um valor padrão, se nenhum estiver presente.

    • ${ndk.minPlatform} é a Plataforma Android de destino mínima solicitada pelo AGP.

  • ninja.targets é uma lista de destinos Ninja específicos que precisam ser criados.

Etapa 2: criar o script de configuração

A responsabilidade mínima do script de configuração (configure-ninja.bat no exemplo anterior) é gerar um arquivo build.ninja que, quando criado com o Ninja, vai compilar e vincular todas as saídas nativas do projeto. Normalmente, eles são itens .o (objeto), .a (arquivo) e .so (objeto compartilhado).

O script de configuração pode gravar o arquivo build.ninja em dois lugares diferentes, dependendo das suas necessidades.

  • Se o AGP puder escolher um local, o script de configuração vai gravar build.ninja no local definido na macro ${ndk.buildRoot}.

  • Se o script de configuração precisar escolher o local do arquivo build.ninja, ele também vai gravar um arquivo chamado build.ninja.txt no local definido na macro ${ndk.buildRoot}. Esse arquivo contém o caminho completo para o build.ninja que o script de configuração gravou.

Estrutura do arquivo build.ninja

Geralmente, a maioria das estruturas que representam com precisão um build C/C++ do Android vão funcionar. Os principais elementos necessários para o AGP e o Android Studio são:

  • a lista de arquivos de origem C/C++ com as sinalizações exigidas pelo Clang para compilar os arquivos;

  • a lista de bibliotecas de saída, que costumam ser itens .so (objeto compartilhado), mas também podem ser .a (arquivo) ou executáveis (sem extensão).

Se você precisa de exemplos de como gerar um arquivo build.ninja, pode analisar a saída do CMake quando o gerador build.ninja é usado.

Veja um exemplo de um modelo build.ninja mínimo:

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

Práticas recomendadas

Além dos requisitos, como lista de arquivos de origem e bibliotecas de saída, temos aqui algumas práticas recomendadas.

Declarar saídas nomeadas com regras phony

Quando possível, recomendamos que a estrutura build.ninja use regras phony para dar nomes legíveis às saídas de build. Por exemplo, se você tem uma saída nomeada como c:/path/to/lib.so, é possível atribuir um nome legível a ela conforme mostrado a seguir.

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

A vantagem de fazer isso é que você pode especificar esse nome como um destino de build no arquivo build.gradle. Por exemplo:

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

Especificar um destino "all"

Quando você especificar um destino all, esse será o conjunto padrão de bibliotecas criadas pelo AGP quando nenhum destino for especificado explicitamente no arquivo build.gradle.

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

Especificar um método de criação alternativo (opcional)

Um caso de uso mais avançado é envolver um sistema de compilação já existente que não seja baseado no Ninja. Nesse caso, ainda é necessário representar todas as origens com as sinalizações correspondentes junto às bibliotecas de saída para que o Android Studio possa apresentar recursos de serviços de idioma adequados, como o preenchimento automático e o acesso à definição. No entanto, é recomendável que o AGP adie para o sistema de compilação durante a criação.

Para fazer isso, você pode usar um resultado de compilação do Ninja com uma extensão .passthrough específica.

Como um exemplo mais concreto, digamos que você queira envolver um MSBuild. O script de configuração geraria o build.ninja como de costume, mas também adicionaria um destino de passagem que define como o AGP vai invocar o 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

Enviar feedback

Esse recurso é experimental, então seu feedback é muito importante. Ele pode ser enviado pelos seguintes canais:

  • Para enviar um feedback geral, adicione um comentário a este bug.

  • Para informar um bug, abra o Android Studio e clique em Help > Submit Feedback. Consulte "Sistemas de compilação C/C++ personalizados" para saber como direcionar o bug.

  • Se você não tiver o Android Studio instalado, registre um bug usando este modelo.