Plataformas

Introducción

Bazel puede compilar y probar código en una variedad de hardware, sistemas operativos y configuraciones del sistema. Esto puede implicar diferentes versiones de herramientas de compilación, como vinculadores y compiladores. Para ayudar a gestionar esta complejidad, Bazel tiene los conceptos de restricciones y plataformas.

Una restricción es una propiedad distintiva de una máquina de producción o compilación. Las restricciones comunes son la arquitectura de la CPU, la presencia o ausencia de una GPU, o la versión de un compilador instalado de forma local. Sin embargo, las restricciones pueden ser cualquier cosa que distinga de manera significativa a las máquinas cuando se coordina el trabajo de compilación.

Una plataforma es una colección de restricciones que especifica una máquina completa. Bazel usa este concepto para permitir que los desarrolladores elijan para qué máquinas desean compilar, qué máquinas deben ejecutar acciones de compilación y prueba, y con qué cadenas de herramientas se deben compilar las acciones de compilación.

Los desarrolladores también pueden usar restricciones para seleccionar propiedades o dependencias personalizadas en sus reglas de compilación. Por ejemplo, "Usa src_arm.cc cuando la compilación se dirija a una máquina Arm".

Tipos de plataformas

Bazel reconoce tres roles que puede desempeñar una plataforma:

  • Host: Es la plataforma en la que se ejecuta Bazel.
  • Ejecución: Es una plataforma que ejecuta acciones de compilación para producir resultados de compilación.
  • Target: Es una plataforma en la que se debe ejecutar el código que se está compilando.

En general, las compilaciones tienen tres tipos de relaciones con las plataformas:

  • Compilaciones para una sola plataforma: Las plataformas de host, ejecución y destino son las mismas. Por ejemplo, compilar en una máquina de desarrollador sin ejecución remota y, luego, ejecutar el objeto binario compilado en la misma máquina.

  • Compilaciones de compilación cruzada: Las plataformas de host y de ejecución son las mismas, pero la plataforma de destino es diferente. Por ejemplo, compilar una app para iOS en una MacBook Pro sin ejecución remota.

  • Compilaciones multiplataforma: Las plataformas de host, ejecución y destino son diferentes. Por ejemplo, compilar una app para iOS en una MacBook Pro y usar máquinas Linux remotas para compilar acciones de C++ que no necesitan Xcode.

Cómo especificar plataformas

La forma más común en que los desarrolladores usan las plataformas es especificar las máquinas de destino deseadas con la marca --platforms:

$ bazel build //:my_linux_app --platforms=//myplatforms:linux_x86

Por lo general, las organizaciones mantienen sus propias definiciones de plataformas, ya que la configuración de las máquinas de compilación varía entre las organizaciones.

Cuando no se establece --platforms, el valor predeterminado es @platforms//host. Esto se define especialmente para detectar automáticamente las propiedades del SO y la CPU de la máquina host, de modo que las compilaciones se dirijan a la misma máquina en la que se ejecuta Bazel. Las reglas de compilación pueden seleccionar estas propiedades con las restricciones @platforms/os y @platforms/cpu.

Restricciones y plataformas generalmente útiles

Para mantener la coherencia del ecosistema, el equipo de Bazel mantiene un repositorio con definiciones de restricciones para las arquitecturas de CPU y los sistemas operativos más populares. Todas estas se definen en https://github.com/bazelbuild/platforms.

Bazel se distribuye con la siguiente definición de plataforma especial: @platforms//host (con el alias @bazel_tools//tools:host_platform). Esta detecta automáticamente las propiedades del SO y la CPU de la máquina en la que se ejecuta Bazel.

Cómo definir restricciones

Las restricciones se modelan con las reglas de compilación constraint_setting y constraint_value.

constraint_setting declara un tipo de propiedad. Por ejemplo:

constraint_setting(name = "cpu")

constraint_value declara un valor posible para esa propiedad:

constraint_value(
    name = "x86",
    constraint_setting = ":cpu"
)

Se puede hacer referencia a ellas como etiquetas cuando se definen plataformas o se personalizan reglas de compilación en ellas. Si los ejemplos anteriores se definen en cpus/BUILD, puedes hacer referencia a la restricción x86 como //cpus:x86.

Si la visibilidad lo permite, puedes extender un constraint_setting existente definiendo tu propio valor para él.

Cómo definir plataformas

La regla de compilación platform define una plataforma como una colección de constraint_values:

platform(
    name = "linux_x86",
    constraint_values = [
        "//oses:linux",
        "//cpus:x86",
    ],
)

Esto modela una máquina que debe tener las restricciones //oses:linux y //cpus:x86.

Las plataformas solo pueden tener un constraint_value para un constraint_setting determinado. Esto significa que, por ejemplo, una plataforma no puede tener dos CPU, a menos que crees otro tipo de constraint_setting para modelar el segundo valor.

Se omiten los destinos incompatibles

Cuando se compila para una plataforma de destino específica, a menudo es conveniente omitir los destinos que nunca funcionarán en esa plataforma. Por ejemplo, es probable que el controlador de tu dispositivo Windows genere muchos errores de compilación cuando se compile en una máquina Linux con //.... Usa el atributo target_compatible_with para indicarle a Bazel qué restricciones de plataforma de destino tiene tu código.

El uso más simple de este atributo restringe un objetivo a una sola plataforma. El destino no se compilará para ninguna plataforma que no satisfaga todas las restricciones. En el siguiente ejemplo, se restringe win_driver_lib.cc a Windows de 64 bits.

cc_library(
    name = "win_driver_lib",
    srcs = ["win_driver_lib.cc"],
    target_compatible_with = [
        "@platforms//cpu:x86_64",
        "@platforms//os:windows",
    ],
)

:win_driver_lib solo es compatible con la compilación con Windows de 64 bits y es incompatible con todo lo demás. La incompatibilidad es transitiva. Cualquier destino que dependa de forma transitiva de un destino incompatible también se considera incompatible.

¿Cuándo se omiten los objetivos?

Los destinos se omiten cuando se consideran incompatibles y se incluyen en la compilación como parte de una expansión de patrones de destino. Por ejemplo, las siguientes dos invocaciones omiten cualquier destino incompatible que se encuentre en una expansión de patrones de destino.

$ bazel build --platforms=//:myplatform //...
$ bazel build --platforms=//:myplatform //:all

Del mismo modo, se omiten las pruebas incompatibles en un test_suite si se especifica test_suite en la línea de comandos con --expand_test_suites. En otras palabras, los destinos de test_suite en la línea de comandos se comportan como :all y .... El uso de --noexpand_test_suites impide la expansión y hace que los objetivos de test_suite con pruebas incompatibles también sean incompatibles.

Si especificas de forma explícita un destino incompatible en la línea de comandos, se generará un mensaje de error y fallará la compilación.

$ bazel build --platforms=//:myplatform //:target_incompatible_with_myplatform
...
ERROR: Target //:target_incompatible_with_myplatform is incompatible and cannot be built, but was explicitly requested.
...
FAILED: Build did NOT complete successfully

Si --skip_incompatible_explicit_targets está habilitado, se omitirán de forma silenciosa los destinos explícitos incompatibles.

Restricciones más expresivas

Para tener más flexibilidad a la hora de expresar restricciones, usa @platforms//:incompatible constraint_value que ninguna plataforma satisface.

Usa select() en combinación con @platforms//:incompatible para expresar restricciones más complicadas. Por ejemplo, úsala para implementar la lógica OR básica. La siguiente marca indica que una biblioteca es compatible con macOS y Linux, pero no con otras plataformas.

cc_library(
    name = "unixish_lib",
    srcs = ["unixish_lib.cc"],
    target_compatible_with = select({
        "@platforms//os:osx": [],
        "@platforms//os:linux": [],
        "//conditions:default": ["@platforms//:incompatible"],
    }),
)

Lo anterior se puede interpretar de la siguiente manera:

  1. Cuando se segmenta para macOS, el destino no tiene restricciones.
  2. Cuando se segmenta para Linux, el objetivo no tiene restricciones.
  3. De lo contrario, el destino tiene la restricción @platforms//:incompatible. Dado que @platforms//:incompatible no forma parte de ninguna plataforma, el destino se considera incompatible.

Para que tus restricciones sean más legibles, usa selects.with_or() de skylib.

Puedes expresar la compatibilidad inversa de manera similar. En el siguiente ejemplo, se describe una biblioteca que es compatible con todo excepto con ARM.

cc_library(
    name = "non_arm_lib",
    srcs = ["non_arm_lib.cc"],
    target_compatible_with = select({
        "@platforms//cpu:arm": ["@platforms//:incompatible"],
        "//conditions:default": [],
    }),
)

Cómo detectar objetivos incompatibles con bazel cquery

Puedes usar IncompatiblePlatformProvider en el formato de salida de Starlark de bazel cquery para distinguir los destinos incompatibles de los compatibles.

Se puede usar para filtrar los objetivos incompatibles. En el siguiente ejemplo, solo se imprimirán las etiquetas de los destinos compatibles. No se imprimen los destinos incompatibles.

$ cat example.cquery

def format(target):
  if "IncompatiblePlatformProvider" not in providers(target):
    return target.label
  return ""


$ bazel cquery //... --output=starlark --starlark:file=example.cquery

Problemas conocidos

Los destinos incompatibles ignoran las restricciones de visibilidad.