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
vspython35.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 ownBUILD
file and reference it usingbuild_file
instead ofbuild_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.