© P.J. McNerney 2020
P. McNerneyBeginning Bazelhttps://doi.org/10.1007/978-1-4842-5194-2_3

3. Your First Bazel Project

P. J. McNerney1 
(1)
Blackhawk, CO, USA
 

Now that you’ve downloaded and set up Bazel, the real fun begins. We’ll start with a small project just to get started and then build (no pun intended) from there.

By the end of this chapter, you will have your first Bazel project up and running and be able to build and test code.

Setting Up Your Workspace

Prior to adding any code, we establish a new Bazel project by creating a WORKSPACE file to a given directory.

Create a directory for your project and create an empty WORKSPACE file:
$ mkdir chapter_03 (or <name of your directory>)
$ cd chapter_03 (or <name of your directory>)
chapter_03$ touch WORKSPACE

The location of the WORKSPACE file should always be at the root of your Bazel project. Within your Bazel project, all paths will be relativized to the WORKSPACE file. As you create your various build targets in various directories, you will be able to refer to them relative to the WORKSPACE file.

However, this is just the tip of the iceberg of the powers of the WORKSPACE file. Later on, we will see how to use the WORKSPACE file to
  • Add new remote code repositories to your workspace (which you can then refer to later on)

  • Add new rules for compiling in different languages

For now, however, the empty WORKSPACE file alone gives us a lot to work with, so we will start from there.

Adding Source Code

While the WORKSPACE file defines the root of your Bazel project, you will define a source directory (possibly multiple source directories) into which to place your code. Code organization is one reason for this, since you will want to have some kind of structure.

However, there is at least one more good reason: Bazel is going to create new sub-directories in the same location as your WORKSPACE directory. We will get into the particulars of these directories shortly, since they pertain to the build products that come out of the Bazel build processes.

Create a directory for your source code:
chapter_03$ mkdir src (or your favorite directory name)

Caution

When considering what to call your directory, do not use one of the following names:

  • bazel-bin

  • bazel-out

  • bazel-testlogs

  • bazel-chapter_03 (or bazel-<name of your directory>)

    If you haven’t guessed yet, these are the special directories that Bazel creates. Creating a directory that aliases with one of these is asking for trouble, so please save yourself a lot of headache by just picking a different name (like src). Also, while the preceding directories are indicative of the current version of Bazel, it is advisable to avoid any directories following a pattern of “bazel-*”.

Hello World, Java Style

Out of the box (at the time of this writing), Bazel supports C++, Java, and Python without any additional configuration. To start, we will create a (slightly modified) Java version of Hello World. (Don’t worry. We’ll get more complex; this is just to get started.)

In your preferred code editor, create the file HelloWorld.java and write the following.
public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, World!);
  }
}
Listing 3-1

A simple Java program

Save that file to disk, under your src directory.

Specifying the BUILD Targets

With the code created, we can now turn our attention to the basic Bazel components required to actually build your work.

Within your src directory, create the file BUILD and write and save the following.
java_binary(
      name = "HelloWorld",
      srcs = ["HelloWorld.java"],
)
Listing 3-2

Your first BUILD file

In this example, HelloWorld is a build target; that is, HelloWorld is a unit that can be identified and built.

Building Your Targets

Having defined something to build, we are now ready to actually build it. Before we do, however, let’s jump back up to root directory, the one with the WORKSPACE file inside of it. Let’s list the contents within:
chapter_03$ ls
WORKSPACE
src (or your favorite directory name)
Additionally, let’s confirm the contents within the src directory. This
chapter_03$ ls src
BUILD    HelloWorld.java

This is the clean state of your Bazel project, where nothing has been built. Let’s change that.

Build the Binary

To build your first project, run the following from the command line:
chapter_03$ bazel build src:HelloWorld
Breaking the arguments to the bazel command down a bit
  • build
    • This specifies that you are building/compiling/assembling the given target.

  • src
    • This specifies the directory which contains your desired build target.

    • In this example, the directory is rather shallow; however, you can (and will) specify any number of valid directory paths to supplement this argument.

  • :HelloWorld
    • This is the actual build target within the src directory.

    • A given directory can have one or more buildable targets in Bazel.

Assuming that all has gone to plan, your output should be something like this:
INFO: Analysed target //src:HelloWorld (19 packages loaded, 550 targets configured).
INFO: Found 1 target...
Target //src:HelloWorld up-to-date:
 bazel-bin/src/HelloWorld.jar
 bazel-bin/src/HelloWorld
INFO: Elapsed time: 0.144s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action.

Running the Binary

Having built your executable, you can run it using the following:
chapter_03$ bazel-bin/src/HelloWorld
Hello, World!

However, for practical development, you will not want to constantly flip between building the executable and then directly executing the binary. Fortunately, you don’t have to; Bazel provides the facility to directly build and run your executable.

Similar to how you built the executable in the first place, you can directly run the executable via
chapter_03% bazel run src:HelloWorld
INFO: Analysed target //src:HelloWorld (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //src:HelloWorld up-to-date:
  bazel-bin/src/HelloWorld.jar
  bazel-bin/src/HelloWorld
INFO: Elapsed time: 0.217s, Critical Path: 0.07s
INFO: 1 process: 1 worker.
INFO: Build completed successfully, 2 total actions
INFO: Build completed successfully, 2 total actions
Hello, World!

Note

One item to notice in both runs is the line regarding (X packages loaded, Y targets configured). This provides a rough indication about the state of the cache for your project. In the first example, these were nonzero values, indicating that work needed to be done on dependencies in order to produce your target. In the second example, both of these were 0, indicating that the build should be fully cached. Bazel loads packages and targets only when something changes, intelligently rebuilding only what is necessary.

Creating and Using Dependencies

Creating a single binary is fine; however, it is certainly not practical for development. In practice, we want to separate our programs into finer grain components. Finer grain components have many advantages, including being more shareable, easier to test, faster to build, and easier to optimize the build.

In this particular case, there isn’t much we can pull out of our original example, so let’s add some new functionality.

Within your src directory, create a new file IntMultiplier.java and add the following code.
public class IntMultiplier {
  private int a;
  private int b;
  public IntMultiplier(int a, int b) {
    this.a = a;
    this.b = b;
  }
  public int GetProduct() {
    return a * b;
  }
}
Listing 3-3

IntMultiplier.java

Don’t add anything to the BUILD file yet; we will first attempt to add our new class to our binary.
public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, World!");
    IntMultiplier im = new IntMultiplier(3, 4);
    System.out.println(im.GetProduct());
  }
}
Listing 3-4

Adding IntMultiplier to HelloWorld.java

Now let’s try to run our build for HelloWorld again:
chapter_03% bazel run src:HelloWorld
INFO: Analysed target //src:HelloWorld (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
ERROR: /Users/pj/Dropbox/Books/Beginning_Bazel/code_samples/chapter_03/src/BUILD:6:1: Building src/HelloWorld.jar (1 source file) failed (Exit 1)
src/HelloWorld.java:5: error: cannot find symbol
        IntMultiplier im = new IntMultiplier(3, 4);
        ^
  symbol:   class IntMultiplier
  location: class HelloWorld
src/HelloWorld.java:5: error: cannot find symbol
        IntMultiplier im = new IntMultiplier(3, 4);
                               ^
  symbol:   class IntMultiplier
  location: class HelloWorld
Target //src:HelloWorld failed to build
Use --verbose_failures to see the command lines of failed build steps.
INFO: Elapsed time: 0.246s, Critical Path: 0.10s
INFO: 0 processes.
FAILED: Build did NOT complete successfully

In this case, the build failed because it was unable to find IntMultiplier. This illustrates one of Bazel’s most important qualities: there is nothing implicit in the build; you need to explicitly specify everything, including all dependencies. Bazel will not automagically find anything in the same directory, package, and so on.

We can solve this issue in one of two ways:
  • Add the new source files to the binary.

  • Create a new library upon which the binary will depend.

We will explore both of these methods.

Adding IntMulitplier.java to java_binary

In this case, we can just add IntMultiplier.java as another source for the HelloWorld build target.
java_binary(
      name = "HelloWorld",
      srcs = [
           "HelloWorld.java",
           "IntMultiplier.java",
      ],
)
Listing 3-5

Adding to the HelloWorld srcs

Now, let’s try rerunning HelloWorld:
chapter_03% bazel run src:HelloWorld
INFO: Analysed target //src:HelloWorld (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //src:HelloWorld up-to-date:
  bazel-bin/src/HelloWorld.jar
  bazel-bin/src/HelloWorld
INFO: Elapsed time: 0.141s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Hello, World!
12

By explicitly listing the files the target depends upon, HelloWorld is able to successfully build and run.

However, while this solution works, it is still sub-optimal; as it stands, IntMultiplier could easily be reused in other places; at the moment, it is locked within the HelloWorld binary.

Creating a java_library Dependency

Instead of adding the file to the HelloWorld build target, let’s instead create an entirely separate dependency. This time, instead of creating a java_binary build target, we are going to introduce a new type of build target, java_library.

As the name implies, the java_library build target is meant to encapsulate some shared collection of Java functionality. Once created, the java_library may then be depended upon by other build targets (which includes other java_library build targets).
java_library(
       name = "LibraryExample",
       srcs = ["IntMultiplier.java"],
)
Listing 3-6

Creating the java_library dependency

Having created a new build target, let’s build it directly:
chapter_03% bazel build src:LibraryExample
INFO: Analysed target //src:LibraryExample (1 packages loaded, 2 targets configured).
INFO: Found 1 target...
Target //src:LibraryExample up-to-date:
  bazel-bin/src/libLibraryExample.jar
INFO: Elapsed time: 0.203s, Critical Path: 0.06s
INFO: 1 process: 1 worker.
INFO: Build completed successfully, 2 total actions
However, as expected and in contrast to our HelloWorld example, we are not able to run this particular build target. Attempting to do so results in the following error:
chapter_03% bazel run src:LibraryExample
ERROR: Cannot run target //src:LibraryExample: Not executable
INFO: Elapsed time: 0.097s
INFO: 0 processes.
FAILED: Build did NOT complete successfully (0 packages loaded)
FAILED: Build did NOT complete successfully (0 packages loaded)

Depending on Build Targets

Now that we have created the build target, we will make HelloWorld depend upon the target.
java_binary(
      name = "HelloWorld",
      srcs = [ "HelloWorld.java"],
      deps = [":LibraryExample"],
)
Listing 3-7

Adding a dependency to HelloWorld

Now let’s rerun HelloWorld:
chapter_03% bazel run src:HelloWorld
INFO: Analysed target //src:HelloWorld (1 packages loaded, 4 targets configured).
INFO: Found 1 target...
Target //src:HelloWorld up-to-date:
  bazel-bin/src/HelloWorld.jar
  bazel-bin/src/HelloWorld
INFO: Elapsed time: 0.955s, Critical Path: 0.78s
INFO: 3 processes: 1 darwin-sandbox, 2 worker.
INFO: Build completed successfully, 7 total actions
INFO: Build completed successfully, 7 total actions
Hello, World!
12

The pattern of binary and library targets in Bazel is a universal pair of constructs, regardless of language. Generally speaking, the number of <insert language>_binary targets you create will be relatively small; they will correspond to the number of output executables you wish to create. In contrast, the number of <insert language>_library build targets you create will be relatively large.

Testing Your Build Targets

One of the chief advantages of creating smaller build units is that they become far easier to test. Having created some modular functionality, let’s set up a test to verify the functionality.

Setting Up Testing Dependencies

Prior to creating a test, we will first need to set up some required dependencies.

Within your project’s root directory, create a new directory, third_party, and two sub-directories therein, hamcrest and junit:
chapter_03$ mkdir third_party
chapter_03$ mkdir third_party/hamcrest
chapter_03$ mkdir third_party/junit
Follow the instructions from the following site https://github.com/junit-team/junit4/wiki/download-and-install to download the following jars:
  • hamcrest-core-1.3.jar

  • junit-4.12.jar

Move the jars into their respective directories under third_party. In order to utilize these jars, we will make use of yet another type of build target, java_import.

Let’s create a new BUILD file to contain the java_import build target.
package(default_visibility = ["//visibility:public"])
java_import(
        name = "junit4",
        jars = [
                "hamcrest/hamcrest-core-1.3.jar",
                "junit/junit-4.12.jar",
       ]
)
Listing 3-8

BUILD file for third_party targets

Note

A sharp observer will note that we have slipped in a new directive, package, into the BUILD file. We will dive further into this in a later chapter to control the visibility of build targets toward other targets. For now, it is sufficient to know that this directive enables the targets contained within this BUILD file to be visible to any other BUILD targets in any other BUILD file.

Save the BUILD file to the third_party directory. You can test that it is set up correctly by running
chapter_03$ bazel build third_party:junit4
INFO: Analysed target //third_party:junit4 (2 packages loaded, 25 targets configured).
INFO: Found 1 target...
Target //third_party:junit4 up-to-date (nothing to build)
INFO: Elapsed time: 0.157s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action

Creating the java_test Build Target

Now let’s create a test for our functionality using the java_test build target.
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class IntMultiplierTest {
        @Test
        public void testIntMultiplier() throws Exception {
          IntMultiplier im = new IntMultiplier(3, 4);
          assertEquals(12, im.GetProduct());
        }
}
Listing 3-9

IntMultiplierTest.java

Save to src/IntMultiplierTest.java.

Now let’s add a new build target to the BUILD file.
java_test(
       name = "LibraryExampleTest",
       srcs = ["IntMultiplierTest.java"],
       deps = [
            ":LibraryExample",
            "//third_party:junit4",
       ],
       test_class = "IntMultiplierTest",
)
Listing 3-10

Adding java_test to BUILD

Run your newly created test:
chapter_03$ bazel test src:LibraryExampleTest
INFO: Build options --collect_code_coverage, --instrumentation_filter, and --test_timeout have changed, discarding analysis cache.
INFO: Analysed target //src:LibraryExampleTest (0 packages loaded, 617 targets configured).
INFO: Found 1 test target...
Target //src:LibraryExampleTest up-to-date:
  bazel-bin/src/LibraryExampleTest.jar
  bazel-bin/src/LibraryExampleTest
INFO: Elapsed time: 2.710s, Critical Path: 2.40s
INFO: 3 processes: 1 darwin-sandbox, 2 worker.
INFO: Build completed successfully, 7 total actions
//src:LibraryExampleTestPASSED in 0.4s
Executed 1 out of 1 test: 1 test passes.
INFO: Build completed successfully, 7 total actions

As expected, the test passes.

Just to verify, let’s add one more test case. This time, let’s initially create a failing test, just to see what happens.
public class IntMultiplierTest {
        @Test
        public void testIntMultiplier() throws Exception {
          IntMultiplier im = new IntMultiplier(3, 4);
          assertEquals(12, im.GetProduct());
        }
        @Test
        public void testIntMultiplier_Failure() throws Exception {
          IntMultiplier im = new IntMultiplier(4, 5);
          assertEquals(21, im.GetProduct());
        }
}
Listing 3-11

Add a failing test

Save the file and re-execute the test:
chapter_03$ bazel test src:LibraryExampleTest
INFO: Analysed target //src:LibraryExampleTest (20 packages loaded, 617 targets configured).
INFO: Found 1 test target...
FAIL: //src:LibraryExampleTest (see <some_local_directory>/execroot/__main__/bazel-out/darwin-fastbuild/testlogs/src/LibraryExampleTest/test.log)
Target //src:LibraryExampleTest up-to-date:
  bazel-bin/src/LibraryExampleTest.jar
  bazel-bin/src/LibraryExampleTest
INFO: Elapsed time: 13.038s, Critical Path: 2.91s
INFO: 3 processes: 1 darwin-sandbox, 2 worker.
INFO: Build completed, 1 test FAILED, 7 total actions
//src:LibraryExampleTestFAILED in 0.3s
 <some_local_directory>/execroot/__main__/bazel-out/darwin-fastbuild/testlogs/src/LibraryExampleTest/test.log
From the (rather obvious) failure, Bazel outputs info to the aforementioned test.log file. Cracking open this file reveals the following.
There was 1 failure:
1) testIntMultiplier_Failure(IntMultiplierTest)
java.lang.AssertionError: expected:<21> but was:<20>
Listing 3-12

Failure found in test.log file

Note that the actual output may be vastly more verbose, but the preceding code is sufficient for us to diagnose and repair the problem. Let’s correct the issue and rerun the test.
public class IntMultiplierTest {
        ...
        @Test
        public void testIntMultiplier_Failure() throws Exception {
          IntMultiplier im = new IntMultiplier(4, 5);
          assertEquals(20, im.GetProduct());
        }
}
Listing 3-13

Correcting the failing test

Rerunning the test:
chapter_03$ bazel test src:LibraryExampleTest
INFO: Analysed target //src:LibraryExampleTest (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //src:LibraryExampleTest up-to-date:
  bazel-bin/src/LibraryExampleTest.jar
  bazel-bin/src/LibraryExampleTest
INFO: Elapsed time: 0.717s, Critical Path: 0.56s
INFO: 2 processes: 1 darwin-sandbox, 1 worker.
INFO: Build completed successfully, 3 total actions
//src:LibraryExampleTestPASSED in 0.3s

Build (and Clean) the World

Before we wrap up, let’s look at a couple more pieces of core Bazel functionality.

Build Everything (In a Directory)

In the preceding examples, we built each of the build targets individually. While this is fine when doing development on individual components, this is obviously not a scalable process.

Bazel has built-in functionality for building multiple types of targets at the same time. For example, instead of building each of the build targets within the src directory, we could order Bazel to build all of them at once by running
chapter_03$ bazel build src:all
INFO: Analysed 3 targets (20 packages loaded, 619 targets configured).
INFO: Found 3 targets...
INFO: Elapsed time: 8.892s, Critical Path: 5.87s
INFO: 9 processes: 6 darwin-sandbox, 3 worker.
INFO: Build completed successfully, 16 total actions

In this case, :all is not a particular build target; it is a meta-target that tells Bazel to literally build all build targets within a given package (i.e., directory).

In a similar fashion, we could tell Bazel to build everything in the third_party directory as well:
chapter_03$ bazel build third_party:all
INFO: Analysed target //third_party:junit4 (13 packages loaded, 520 targets configured).
INFO: Found 1 target...
Target //third_party:junit4 up-to-date (nothing to build)
INFO: Elapsed time: 1.467s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action

The :all target works not only for building but for all of the Bazel commands (e.g., bazel test <insert target>).

It might already be obvious, but do not name any of your build targets “all.” This will only lead to confusion.

Build Everything (At This Directory and Below)

Once again, the preceding build all command works great when dealing with particular directories; however, it would again become tedious if having to build for all directories in this manner. Fortunately, Bazel once again comes to our rescue with yet another command to help out.

Run the following command from your workspace root:
chapter_03$ bazel build ...
INFO: Analysed 4 targets (20 packages loaded, 620 targets configured).
INFO: Found 4 targets...
INFO: Elapsed time: 7.234s, Critical Path: 5.78s
INFO: 9 processes: 6 darwin-sandbox, 3 worker.
INFO: Build completed successfully, 16 total actions

This time, the "..." meta-target is telling Bazel to build everything at the current directory as well as everything below this directory. When executed at the root level of your workspace, this will build everything in your workspace. Use caution when building like this, although this may very well be a great way to start your morning after updating your local repository.

As with the :all meta-target, the "..." meta-target will also work with the other Bazel commands (e.g., test).

Additionally, you can scope “…” to particular directories. For instance, you could have used “bazel build src/…” in order to build everything under the src directory.

Clean (Mostly) Everything

As good as Bazel is at managing dependencies, you may get to some point in time where you need to just clean the world and start over. Cleaning in Bazel is as simple as
chapter_03$ bazel clean
INFO: Starting clean.

That’s really it. If you do a quick ls on your root directory, you will notice that none of the bazel-* directories are there any longer; all of the outputs, caches, and so on have been removed. Of course, they will return upon your next Bazel command that builds your targets.

Final Word

Congratulations! You have just created and wired together your first set of Bazel targets, encompassing a host of different pieces of functionality:
  • java_binary
    • Representing and creating a Java executable

  • java_library
    • Encapsulating a shareable piece of Java functionality

  • java_import
    • Wrapping one or more preexisting jar files into a unit that can be depended upon

  • java_test
    • Creating a test for verifying the expected behavior of the java_library

With even this small subset of Bazel build targets, you have sufficient functionality to create, organize, test, and run a Java program.

Notably, this chapter focused exclusively on Java targets in order to illustrate Bazel functionality. However, the pattern of {language}_binary, {language}_library, and {language}_test will become familiar for the various languages that Bazel (and its extensions) supports.

For example:
  • C/C++ (built-in support from Bazel)
    • cc_binary, cc_library, cc_test

  • Python (built-in support from Bazel)
    • py_binary, py_library, py_test

  • Go (supplied by external rules)
    • go_binary, go_library, go_test

Of course, each language supported by Bazel may also have some language-specific constructs (e.g., java_import); however, even in these cases, there are features that are largely common to all types of build targets (e.g., name, visibility, dependencies, etc.).

In the following chapters, we will focus less on a specific language and dive further into some of the structural elements of Bazel itself, namely, around the BUILD and the WORKSPACE files.

Exercise – Python Helloworld

Throughout this chapter, we have only been focused on creating Java targets. However, out of the box, Bazel has the ability to target Java, Python, and C++. Now that you have done the HelloWorld exercise for Java, create it using Python.

Since one of the hallmarks of Bazel is handling multiple languages at the same time, you can create a similar set of HelloWorld Python targets within the same BUILD file as your Java targets. Practically speaking, you are unlikely to do this in real life; however, it does illustrate Bazel’s ability to handle multiple targets, across languages, within the same BUILD file. Your Python executable will end up in a py_binary build target.

Finally, you can also create similar IntMultiplier functionality in its own py_library build target as well as a corresponding set of tests within its py_test build target. Unlike Java, Python comes “batteries included” and packages up its own unit test framework, obviating the need to create something similar to the junit4 build target for Java.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.233.94