Instructivo de Bazel: Configura cadenas de herramientas de C++

En este instructivo, se usa una situación de ejemplo para describir cómo configurar cadenas de herramientas de C++ para un proyecto. Está basado en un proyecto de ejemplo de C++ que compila sin errores mediante clang.

Qué aprenderás

En este instructivo, aprenderás a hacer lo siguiente:

  • Cómo configurar el entorno de compilación
  • Cómo configurar la cadena de herramientas de C++
  • Crea una regla de Starlark que proporcione configuración adicional para el cc_toolchain de modo que Bazel pueda compilar la aplicación con clang.
  • Para confirmar el resultado esperado, ejecuta bazel build --config=clang_config //main:hello-world en una máquina Linux.
  • Compila la aplicación de C++

Antes de comenzar

Cómo configurar el entorno de compilación

En este instructivo, se supone que estás en Linux, que compilaste aplicaciones de C++ y que instalaste las herramientas y bibliotecas adecuadas. En este instructivo, se usa clang version 9.0.1, que puedes instalar en tu sistema.

Configura tu entorno de compilación de la siguiente manera:

  1. Si aún no lo hiciste, descarga e instala Bazel 0.23 o una versión posterior.

  2. Descarga el proyecto de ejemplo de C++ de GitHub y colócalo en un directorio vacío de tu máquina local.

  3. Agrega el siguiente destino cc_binary al archivo main/BUILD:

    cc_binary(
        name = "hello-world",
        srcs = ["hello-world.cc"],
    )
    
  4. Crea un archivo .bazelrc en la raíz del directorio del lugar de trabajo con el siguiente contenido para habilitar el uso de la marca --config:

    # Use our custom-configured c++ toolchain.
    
    build:clang_config --crosstool_top=//toolchain:clang_suite
    
    # Use --cpu as a differentiator.
    
    build:clang_config --cpu=k8
    
    # Use the default Bazel C++ toolchain to build the tools used during the
    # build.
    
    build:clang_config --host_crosstool_top=@bazel_tools//tools/cpp:toolchain
    

Para una entrada build:{config_name} --flag=value, la marca de línea de comandos --config={config_name} está asociada con esa marca en particular. Consulta la documentación de las marcas usadas: crosstool_top, cpu y host_crosstool_top.

Cuando compilas tu objetivo con bazel build --config=clang_config //main:hello-world, Bazel usa tu cadena de herramientas personalizada de cc_toolchain_suite //toolchain:clang_suite. El paquete puede enumerar diferentes cadenas de herramientas para distintas CPU y, por eso, se diferencia con la marca --cpu=k8.

Debido a que Bazel usa muchas herramientas internas escritas en C++ durante la compilación, como process-wrapper, se especifica la cadena de herramientas de C++ predeterminada preexistente para la plataforma host, de modo que se compilen con esa cadena en lugar de la creada en este instructivo.

Configura la cadena de herramientas de C++

Para configurar la cadena de herramientas de C++, compila la aplicación de manera repetida y elimina cada error uno por uno, como se describe a continuación.

  1. Ejecuta la compilación con el siguiente comando:

    bazel build --config=clang_config //main:hello-world
    

    Debido a que especificaste --crosstool_top=//toolchain:clang_suite en el archivo .bazelrc, Bazel muestra el siguiente error:

    No such package `toolchain`: BUILD file not found on package path.
    

    En el directorio del lugar de trabajo, crea el directorio toolchain para el paquete y un archivo BUILD vacío dentro del directorio toolchain.

  2. Vuelve a ejecutar la compilación. Debido a que el paquete toolchain aún no define el destino clang_suite, Bazel arroja el siguiente error:

    No such target '//toolchain:clang_suite': target 'clang_suite' not declared
    in package 'toolchain' defined by .../toolchain/BUILD
    

    En el archivo toolchain/BUILD, define un grupo de archivos vacío de la siguiente manera:

    package(default_visibility = ["//visibility:public"])
    
    filegroup(name = "clang_suite")
    
  3. Vuelve a ejecutar la compilación. Bazel arroja el siguiente error:

    '//toolchain:clang_suite' does not have mandatory providers: 'ToolchainInfo'
    

    Bazel descubrió que la marca --crosstool_top apunta a una regla que no proporciona el proveedor ToolchainInfo necesario. Por lo tanto, debes apuntar --crosstool_top a una regla que proporcione ToolchainInfo, es decir, la regla cc_toolchain_suite. En el archivo toolchain/BUILD, reemplaza el grupo de archivos vacío por lo siguiente:

    cc_toolchain_suite(
        name = "clang_suite",
        toolchains = {
            "k8": ":k8_toolchain",
        },
    )
    

    El atributo toolchains asigna automáticamente los valores --cpu (y también --compiler cuando se especifica) a cc_toolchain. Aún no defines ningún destino de cc_toolchain, y Bazel se quejará de eso en breve.

  4. Vuelve a ejecutar la compilación. Bazel arroja el siguiente error:

    Rule '//toolchain:k8_toolchain' does not exist
    

    Ahora, debes definir objetivos de cc_toolchain para cada valor en el atributo cc_toolchain_suite.toolchains. Agrega lo siguiente al archivo toolchain/BUILD:

    filegroup(name = "empty")
    
    cc_toolchain(
        name = "k8_toolchain",
        toolchain_identifier = "k8-toolchain",
        toolchain_config = ":k8_toolchain_config",
        all_files = ":empty",
        compiler_files = ":empty",
        dwp_files = ":empty",
        linker_files = ":empty",
        objcopy_files = ":empty",
        strip_files = ":empty",
        supports_param_files = 0,
    )
    
  5. Vuelve a ejecutar la compilación. Bazel arroja el siguiente error:

    Rule '//toolchain:k8_toolchain_config' does not exist
    

    A continuación, agrega un objetivo ":k8_toolchain_config" al archivo toolchain/BUILD:

    filegroup(name = "k8_toolchain_config")
    
  6. Vuelve a ejecutar la compilación. Bazel arroja el siguiente error:

    '//toolchain:k8_toolchain_config' does not have mandatory providers:
    'CcToolchainConfigInfo'
    

    CcToolchainConfigInfo es un proveedor que usas para configurar tus cadenas de herramientas de C++. Para corregir este error, crea una regla de Starlark que proporcione CcToolchainConfigInfo a Bazel. Para ello, crea un archivo toolchain/cc_toolchain_config.bzl con el siguiente contenido:

    def _impl(ctx):
        return cc_common.create_cc_toolchain_config_info(
            ctx = ctx,
            toolchain_identifier = "k8-toolchain",
            host_system_name = "local",
            target_system_name = "local",
            target_cpu = "k8",
            target_libc = "unknown",
            compiler = "clang",
            abi_version = "unknown",
            abi_libc_version = "unknown",
        )
    
    cc_toolchain_config = rule(
        implementation = _impl,
        attrs = {},
        provides = [CcToolchainConfigInfo],
    )
    

    cc_common.create_cc_toolchain_config_info() crea el proveedor necesario CcToolchainConfigInfo. Para usar la regla cc_toolchain_config, agrega una declaración de carga a toolchains/BUILD:

    load(":cc_toolchain_config.bzl", "cc_toolchain_config")
    

    Además, reemplaza el grupo de archivos "k8_toolchain_config" por una declaración de una regla cc_toolchain_config:

    cc_toolchain_config(name = "k8_toolchain_config")
    
  7. Vuelve a ejecutar la compilación. Bazel arroja el siguiente error:

    .../BUILD:1:1: C++ compilation of rule '//:hello-world' failed (Exit 1)
    src/main/tools/linux-sandbox-pid1.cc:421:
    "execvp(toolchain/DUMMY_GCC_TOOL, 0x11f20e0)": No such file or directory
    Target //:hello-world failed to build`
    

    En este punto, Bazel tiene suficiente información para intentar compilar el código, pero aún no sabe qué herramientas usar para completar las acciones de compilación requeridas. Modificarás la implementación de la regla de Starlark para indicarle a Bazel qué herramientas usar. Para ello, necesitas el constructor tool_path() de @bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl:

    # toolchain/cc_toolchain_config.bzl:
    # NEW
    load("@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl", "tool_path")
    
    def _impl(ctx):
        tool_paths = [ # NEW
            tool_path(
                name = "gcc",
                path = "/usr/bin/clang",
            ),
            tool_path(
                name = "ld",
                path = "/usr/bin/ld",
            ),
            tool_path(
                name = "ar",
                path = "/usr/bin/ar",
            ),
            tool_path(
                name = "cpp",
                path = "/bin/false",
            ),
            tool_path(
                name = "gcov",
                path = "/bin/false",
            ),
            tool_path(
                name = "nm",
                path = "/bin/false",
            ),
            tool_path(
                name = "objdump",
                path = "/bin/false",
            ),
            tool_path(
                name = "strip",
                path = "/bin/false",
            ),
        ]
        return cc_common.create_cc_toolchain_config_info(
            ctx = ctx,
            toolchain_identifier = "local",
            host_system_name = "local",
            target_system_name = "local",
            target_cpu = "k8",
            target_libc = "unknown",
            compiler = "clang",
            abi_version = "unknown",
            abi_libc_version = "unknown",
            tool_paths = tool_paths, # NEW
        )
    

    Asegúrate de que /usr/bin/clang y /usr/bin/ld sean las rutas de acceso correctas para tu sistema.

  8. Vuelve a ejecutar la compilación. Bazel arroja el siguiente error:

     ..../BUILD:3:1: undeclared inclusion(s) in rule '//main:hello-world':
     this rule is missing dependency declarations for the following files included by 'main/hello-world.cc':
     '/usr/include/c++/9/ctime'
     '/usr/include/x86_64-linux-gnu/c++/9/bits/c++config.h'
     '/usr/include/x86_64-linux-gnu/c++/9/bits/os_defines.h'
     ....
    

    Bazel necesita saber dónde buscar los encabezados incluidos. Hay varias maneras de resolver esto, como usar el atributo includes de cc_binary, pero aquí se resuelve a nivel de la cadena de herramientas con el parámetro cxx_builtin_include_directories de cc_common.create_cc_toolchain_config_info. Ten en cuenta que, si usas una versión diferente de clang, la ruta de inclusión será diferente. Esas rutas también pueden ser diferentes según la distribución.

    Modifica el valor que se muestra en toolchain/cc_toolchain_config.bzl para que se vea de la siguiente manera:

     return cc_common.create_cc_toolchain_config_info(
          ctx = ctx,
          cxx_builtin_include_directories = [ # NEW
            "/usr/lib/llvm-9/lib/clang/9.0.1/include",
            "/usr/include",
          ],
          toolchain_identifier = "local",
          host_system_name = "local",
          target_system_name = "local",
          target_cpu = "k8",
          target_libc = "unknown",
          compiler = "clang",
          abi_version = "unknown",
          abi_libc_version = "unknown",
          tool_paths = tool_paths,
     )
    
  9. Ejecuta el comando de compilación nuevamente y verás un error como el siguiente:

    /usr/bin/ld: bazel-out/k8-fastbuild/bin/main/_objs/hello-world/hello-world.o: in function `print_localtime()':
    hello-world.cc:(.text+0x68): undefined reference to `std::cout'
    

    Esto se debe a que al vinculador le falta la biblioteca estándar C++ y no puede encontrar sus símbolos. Hay muchas formas de solucionar este problema, como usar el atributo linkopts de cc_binary. Aquí se resuelve asegurándote de que cualquier destino que use la cadena de herramientas no tenga que especificar esta marca.

    Copia el siguiente código en cc_toolchain_config.bzl:

      # NEW
      load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
      # NEW
      load(
          "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
          "feature",
          "flag_group",
          "flag_set",
          "tool_path",
      )
    
      all_link_actions = [ # NEW
          ACTION_NAMES.cpp_link_executable,
          ACTION_NAMES.cpp_link_dynamic_library,
          ACTION_NAMES.cpp_link_nodeps_dynamic_library,
      ]
    
      def _impl(ctx):
          tool_paths = [
              tool_path(
                  name = "gcc",
                  path = "/usr/bin/clang",
              ),
              tool_path(
                  name = "ld",
                  path = "/usr/bin/ld",
              ),
              tool_path(
                  name = "ar",
                  path = "/bin/false",
              ),
              tool_path(
                  name = "cpp",
                  path = "/bin/false",
              ),
              tool_path(
                  name = "gcov",
                  path = "/bin/false",
              ),
              tool_path(
                  name = "nm",
                  path = "/bin/false",
              ),
              tool_path(
                  name = "objdump",
                  path = "/bin/false",
              ),
              tool_path(
                  name = "strip",
                  path = "/bin/false",
              ),
          ]
    
          features = [ # NEW
              feature(
                  name = "default_linker_flags",
                  enabled = True,
                  flag_sets = [
                      flag_set(
                          actions = all_link_actions,
                          flag_groups = ([
                              flag_group(
                                  flags = [
                                      "-lstdc++",
                                  ],
                              ),
                          ]),
                      ),
                  ],
              ),
          ]
    
          return cc_common.create_cc_toolchain_config_info(
              ctx = ctx,
              features = features, # NEW
              cxx_builtin_include_directories = [
                  "/usr/lib/llvm-9/lib/clang/9.0.1/include",
                  "/usr/include",
              ],
              toolchain_identifier = "local",
              host_system_name = "local",
              target_system_name = "local",
              target_cpu = "k8",
              target_libc = "unknown",
              compiler = "clang",
              abi_version = "unknown",
              abi_libc_version = "unknown",
              tool_paths = tool_paths,
          )
    
      cc_toolchain_config = rule(
          implementation = _impl,
          attrs = {},
          provides = [CcToolchainConfigInfo],
      )
    
  10. Si ejecutas bazel build --config=clang_config //main:hello-world, finalmente debería compilarse.

Revisa tu trabajo

En este instructivo, aprendiste a configurar una cadena de herramientas básica de C++, pero las cadenas de herramientas son más potentes que este ejemplo simple.

Las conclusiones clave son las siguientes: - Debes especificar una marca --crosstool_top en la línea de comandos, que debería apuntar a un elemento cc_toolchain_suite. - Puedes crear un acceso directo para una configuración específica mediante el archivo .bazelrc. - cc_toolchain_suite puede incluir cc_toolchains para diferentes CPU y compiladores. Puedes usar marcas de línea de comandos, como --cpu, para diferenciarlos. - Debes informarle al conjunto de herramientas dónde se encuentran las herramientas. En este instructivo, hay una versión simplificada en la que puedes acceder a las herramientas desde el sistema. Si te interesa un enfoque más autónomo, puedes leer sobre los lugares de trabajo aquí. Tus herramientas podrían provenir de un lugar de trabajo diferente, y tendrías que hacer que sus archivos estén disponibles para cc_toolchain con dependencias de destino en los atributos, como compiler_files. También se deberá cambiar el tool_paths. - Puedes crear funciones para personalizar qué marcas se deben pasar a diferentes acciones, ya sea de vinculación o de cualquier otro tipo de acción.

Lecturas adicionales

Para obtener más detalles, consulta Configuración de la cadena de herramientas C++