En el artículo anterior, se introdujeron los principios básicos de funcionamiento de Bazel y los conceptos relacionados. En este artículo, continuaremos explicando cómo proyectos existentes que utilizan Makefile pueden migrar al sistema de construcción de Bazel.
Bazel ofrece diversas extensiones, incluyendo soporte para la transición desde Makefile a su sistema de construcción.
Una vez más, sus características principales:
- Soporte multi-lenguaje, extensible a cualquier lenguaje de programación. El DSL de extensión es Starlark, un subconjunto de Python fácil de aprender. Esta es una ventaja que otros sistemas como CMake no ofrecen.
- Soporte para caché.
- Construcción distribuida.
- Construcción incremental mínima. En sistemas grandes, un desarrollador puede estar responsable de un solo componente que depende de otros. Al actualizar este componente, solo se debería reconstruir él y sus dependencias, haciendo el proceso más rápido. Bazel soporta este enfoque.
0x02 Problemas de Makefile
Como se mencionó anteriormente, Makefile funciona bien para proyectos pequeños, pero presenta problemas a escala:
- Gestión compleja de dependencias entre componentes. Makefile generalmente requiere colocar todo el código fuente en un único directorio, manejado desde un nivel superior. Estas dependencias solo son conocidas por experimentados desarrolladores; los nuevos deben ensayar y error, construyendo manualmente cuando encuentran dependencias faltantes.
- Control difícil de la construcción incremental. Esto está relacionado con el punto anterior; como las dependencias no se construyen automáticamente, la construcción incremental tampoco es efectiva.
- Velocidad insuficiente y falta de caché. Makefile no soporta caché de forma nativa, requiriendo implementaciones adicionales por parte del desarrollador.
0x03 Problemas a Resolver
Aunque Bazel es compatible con Makefile, no lo soporta directamente. Es necesario entender los métodos de extensión de Bazel. Los problemas principales son:
- Las reglas de Bazel requieren definir entradas y salidas explícitamente. En Makefile, las salidas generalmente no se definen explícitamente. Por ejemplo:
all:
gcc hello.c
- O como este caso donde se ejecuta un script:
install:
bash -x make.sh
- Makefile generalmente se ejecuta en el directorio actual. Las entradas y salidas definidas en Makefile son relativas al directorio actual, mientras que Bazel se ejecuta desde el directorio raíz donde se encuentra WORKSPACE, lo que no es ideal para Makefile.
0x04 Solución de Transición
La solución propuesta tiene los siguientes puntos clave:
- Utilizar variables de entorno del sandbox de Bazel para mantener las variables de entorno de Makefile. Estas pueden incluir variables de instalación como INSTALL_ROOT o variables de compilación/enlace como LD_LIBRARY_PATH.
- Utilizar la regla genrule para cambiar rápidamente al directorio del Makefile y ejecutar make.
El primer punto se puede lograr fácilmente con la opción --action_env durante la construcción de Bazel. El segundo punto requiere un enfoque más elaborado pero es sencillo de implementar.
Para lograr esto, podemos definir un archivo def.bzl que introduce una variable de entorno BUILD_PATH en los archivos BUILD que lo referencian:
def _var_provider_rule_impl(ctx):
build_file_location = ctx.build_file_path
directory_location, _ = build_file_location.rsplit('/', 2)
return [platform_common.TemplateVariableInfo({"BUILD_PATH": directory_location})]
env_provider_rule = rule(
implementation = _var_provider_rule_impl,
attrs = {"env_value": attr.string()}
)
Luego, en cada directorio con Makefile, creamos un archivo BUILD similar a este:
load("//:def.bzl", "env_provider_rule")
env_provider_rule(
name="set_directory_path",
env_value=""
)
filegroup(
name="sources",
srcs=glob(["**"])
)
genrule(
name = "build",
srcs=["sources"],
cmd="cd $(BUILD_PATH) && make && cd - && touch $(RULEDIR)/result.txt",
visibility=["//visibility:public"],
outs=["result.txt"],
toolchains=[":set_directory_path"]
)
La explicación del parámetro cmd:
- Cambiar al directorio donde se encuentra el archivo BUILD.
- Ejecutar make (o el comando necesario).
- Volver al directorio anterior (donde está WORKSPACE).
- Crear un archivo de salida ficticio result.txt. RULE_DIR es el directorio de salida de la regla, relativo al directorio de trabajo.
Finalmente, la ejecución se realiza con un comando como:
bazel build --sandbox_writable_path=$INSTALL_ROOT --action_env=C_INCLUDE_PATH=$INSTALL_ROOT/include --action_env=INSTALL_ROOT=$INSTALL_ROOT //src:build
0x05 Conclusión
Esta implementación puede no ser la óptima, pero es la más directa actualmente. Tiene algunos inconvenientes:
- Los productos generados son ficticios y no son percibidos como "sonidos" por Bazel, lo que impide el caché de los productos generados en INSTALL_ROOT.
- Al no ser percibidos, si INSTALL_ROOT es eliminado, Bazel podría no realizar ninguna acción constructiva.
Para proyectos grandes que requieren caché, se pueden generar productos reales y copiarlos a RULE_DIR. Sin embargo, el problema de la eliminación de INSTALL_ROOT no se puede resolver completamente mediante técnicas técnicas.