Compilando projetos multiplataforma com Docker

Esses dias voltei a fazer alguns experimentos com o Docker para compilação multiplataforma da selene, principalmente para Android e Emscripten.

Atualmente uso alguns scripts de CMake auxiliares para fazer o Setup desses ambientes, tanto no Linux, quanto no Windows. Alio isso ao uso de toolchains para facilitar o processo, faço algo como:

cmake -P ./cmake/scripts/SetupEmscripten.cmake
cmake -S $SOURCE_DIR -B $BUILD_DIR -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/Emscripten.cmake

cmake --build $BUILD_DIR --target package --config Release

Para empacotar para Emscripten, por exemplo. Para Android é parecido, porém uso o gradle para gerar o APK final, e no Linux também gosto de empacotar e distribuir como AppImage.

Configurei de modo que a compilação para Emscripten e Android funcionem tanto no Windows quanto no Linux, então é bem útil. Para gerar os arquivos finais para cada plataforma, posso automatizar com scripts, bash no Unix e batch no Windows, apesar que com o WSL (Windows Subsystem for Linux) fica muito mais fácil de reaproveitar o bash. Ainda tem um problema com esse método, compilação multi-arquitetura continua sendo um saco. Quando você precisa somente do compilador e da toolchain é ótimo, mas quando são necessários alguns arquivos de sistema, como no meu caso que uso o SDL2 e preciso das libs do X11 e coisas assim, já fica um pouco mais complicado. É possível com sysroot ou virtualização, mas já é um pouco mais trabalhoso manter tudo, porém não impossível.

Esses dias descobri o qemu-user-static que funciona em conjunto com o Docker através do binfmt_misc, que é a funcionalidade do kernel que permite fazer esse acessor ao qemu meio que como um sysroot, para rodar os binários da plataforma de target. É uma solução ótima para desenvolvimento multi-arquitetura, pois uma vez que a imagem que você esteja utilizando também tenha suporte para multiplas arquiteturas nativamente, como é o caso do Debian, por exemplo; é possível a partir do mesmo container rodar ao mesmo tempo uma build para diferentes arquiteturas com um único comando. Algo simples como:

docker run --rm -v ./myrepo:/source -v ./dist:/dist --platform linux/arm64,linux/amd64 my_container

E assim no final do processo recebo os arquivos para cada plataforma dentro da pasta ./dist. Fora que enquanto isso também posso compilar para Android e Emscripten e seus respectivos containers. Então achei bem mais interessante nesse sentido, até mesmo para disponibilizar como ambientes de teste e compilação de projetos finalizados. Já que minha ideia é desenvolver uma game engine/framework, então o usuário precisa de alguma forma de empacotar a build final com os arquivos do seu projeto. Com o Docker acho que fica mais fácil de distribuir algo nesse sentido, posso fazer builds de empacotamento final com:

docker run --rm -e SELENE_APP_NAME="Roguelike" -e SELENE_APP_NAMESPACE="org.selene.Roguelike" -v ./game:/game -v ./dist:/dist my_container-android

Ou algo assim, acho que é uma ideia interessante. Outra vantagem de utilizar Docker no desenvolvimento Android é exatamente poder usar variáveis de ambiente para controlar as informações da build final, como o nome do aplicativo. Tudo bem que há N formas de fazer isso, porém no gradle é bem útil poder usar o System.getenv('VAR_NAME'), com isso posso garantir acesso à outras informações dinamicamente, como para quais ABIs do Android eu devo compilar, por exemplo.

externalNativeBuild {
  cmake {
    arguments "-DANDROID_APP_PLATFORM=android-21", "-DANDROID_STL=c++_static", "-DUSE_SDL_SOURCE=ON"
	def abiEnv = System.getenv("SELENE_ANDROID_ABIS")
	if (abiEnv != null) {
    	abiFilters abiEnv.split(",")
	} else {
    	abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
	}
  }
}

De novo, isso poderia facilmente ser dinamizado com scripts bash/batch, inclusive talvez seja uma solução muito mais interessante para quem já tem um ambiente Android com NDK configurado, por exemplo. Mas para isolar em containers, e principalmente, facilitar a distribuição (já que eu posso tanto distribuir o Dockefile, quanto subir minhas imagens finais no Docker Hub), o Docker é uma solução muito mais interessante.

Para compilação multiplataforma no que tange outros sistemas Unix como BSD e MacOS, estou fazendo alguns testes com compilação remota, ou seja, via SSH a partir de um sistema rodando em uma máquina virtual. Atualmente estou fazendo alguns testes com o BSD que por ser aberto é muito mais fácil de testar, porém a compilação está falhando com alguns erros no OpenGL, estou achando que por conta do GLAD, porém ainda vou parar pra investigar isso.

O ideal mesmo é conseguir compilar para o MacOS, inclusive a possibilidade de compilar para MacOS foi um dos principais motivos pelo qual passei a adotar o GitHub Actions, que é fato dele possibilitar testes automatizados em um ambiente MacOS também. Estava sendo útil principalmente para gerar os pacotes com o actions/upload-artifact, entretanto eu nunca cheguei a testar em um ambiente Mac, então não posso dizer se funciona ou não. Porém caso eu tenha êxito instalando o sistema em uma VM, poderei fazer mais testes nesse sentido.

No mais é isso, fica aí a dica, mais pra frente vou ver se faço um tutorial de como configurar o Docker para funcionar com o qemu-user-static (o que é bem simples na verdade), e gerar builds para múltiplas arquiteturas.