Cross Platform Compile using Bazel

I've seen this come up often on the Bazel Discuss forums and Stack Overflow and wanted to make a quick blog post about it. For reference, all code is contained within this repo.

C based projects often need to work cross platform and need special linking or compiling settings in order to achive this. The setup below shows how to configure your Bazel project so that Bazel does the "right thing" when compiling for the host platform.

Let's say you want to embed a Python application within your C application. Some of the challenges we will see when compiling are:

  • Python is installed in a different place on Windows/Linux, we need Bazel to choose the right location
  • The Python library we need to link against is named differently on the host systems (libpython3.5.so vs python35.lib)
  • Directory containing Python header files is slightly different per host system

Setting Up the Bazel Workspace

First off, we need to tell Bazel about the installed Python. To tell Bazel about any third party dependency that is not checked into your git repository you typically use one of the Workspace Rules.

In our case we'll use new_local_repository because the directory is already on our machine and it doesn't have it's own Bazel BUILD file.

A completed WORKSPACE file to support Windows and Linux can be seen below:

new_local_repository(  
    name = "python_linux",
    path = "/usr",
    build_file_content = """
cc_library(  
    name = "python35-lib",
    srcs = ["lib/python3.5/config-3.5m-x86_64-linux-gnu/libpython3.5.so"],
    hdrs = glob(["include/python3.5/*.h"]),
    includes = ["include/python3.5"],
    visibility = ["//visibility:public"]
)
    """
)

new_local_repository(  
    name = "python_win",
    path = "C:/Python35",
    build_file_content = """
cc_library(  
    name = "python35-lib",
    srcs = ["libs/python35.lib"],
    hdrs = glob(["include/*.h"]),
    includes = ["include/"],
    visibility = ["//visibility:public"]
)
    """
)

To break this down, each host OS contains:

  • Name: a unique name that defines this third party dependeny
  • Path: the path to the root of the external dependency
  • build_file_content: essentialy the contents of a BUILD file for the third party dependency. If this becomes too complex you can put it in it's own BUILD file and reference it using build_file instead of build_file_content

The BUILD file contents consists of:

  • Name: a simple alias we can use to reference the library throughout Bazel
  • srcs: the actual precompiled library to link against
  • hdrs: a list of header files that anything compiling against will need
  • includes: an include path to add to anything compiling with this library
  • visibility: making them public allows any BUILD file in Bazel to reference them

Setting Up a Build

Now that Bazel knows about our precompiled libraries we need to create an application to link against them. I found a quick and dirty C program that runs embedded python and created the main.c below:

#include "Python.h"

int main(int argc, char *argv[])  
{
  Py_SetProgramName(argv[0]);  /* optional but recommended */
  Py_Initialize();
  PyRun_SimpleString("from time import time,ctime\n"
                     "print('Today is',ctime(time()))\n");
  Py_Finalize();
  return 0;
}

One thing to notice about the C program is that it does #include "Python.h". This will work in our cross platform build because each Python library exposed it's include directory using the includes attribute of the cc_library rule.

To build this C program we can create the following BUILD file:

config_setting(  
    name = "linux_x86_64",
    values = {"cpu": "k8"},
    visibility = ["//visibility:public"],
)

config_setting(  
    name = "windows",
    values = {"cpu": "x64_windows"},
    visibility = ["//visibility:public"],
)

cc_binary(  
    name="python-test",
    srcs = [
        "main.c",
    ],
    deps = select({
        "//:linux_x86_64": [
            "@python_linux//:python35-lib"
        ],
        "//:windows": [
            "@python_win//:python35-lib"
        ]
    })
)

The real power of Bazel here is in the config_setting and select() functionality.

The BUILD file starts by defining two different config settings. Basically, the first tells Bazel that when cpu=k8 that a Linux build is happening and when cpu=x64_windows that a Windows buid is happening. Bazel automatically sets those variables when building on the appropriate hosts.

The cc_binary rule includes the main.c we mentioned earlier but to actually reference the library it uses the select() function. The select function takes in a dictionary where the keys are configurations and the values are the return values. select will basically return the correct value for whatever configuration listed in the keys is active. Here we use it to select the appropriate library to link against. You could also use it if you need custom copts, linkopts, srcs or any other attribute of rules.

Should I Just Check it In?

One other approach that simplifies some of the setup is to just check in the Python libraries and headers into your repository. There are loads of JARs and other precompiled code in the Bazel repository, so it's defintely a common practice. However, some teams may be relucatant to check in so many binary artifacts into their repository. Either way, the select() function is still needed to choose the correct library at link time.

In conclusion, Bazel is a powerful tool and changing how I manage large builds that need to work across platforms (and even architectures). With some added logic, you can manage a cross platform build easily with Bazel.

I've seen this come up often on the Bazel Discuss forums and Stack Overflow and wanted to make a quick blog post about it. For reference, all code is contained within this repo. C based projects often need to work cross platform and need special linking or compiling settings in…

Read More