The build.py project descriptor

The build.py anatomy

A build.py project descriptor consists of several parts:

Imports

It’s python code after all, so PyBuilder functions we use must be imported first.

import os
from pybuilder.core import task, init, use_plugin, Author

Plugin imports

Through usage of the use_plugin function, a plugin is loaded (its initializers are registered for execution and any tasks are added to the available tasks).

use_plugin("python.core")
use_plugin("python.pycharm")

Project fields

Assigning to variables in the top-level of the build.py will set the corresponding fields on the project object. Some of these fields are standardized (like authors, name and version).

authors = [Author('John Doe', 'john@doe.invalid'),
           Author('Jane Doe', 'jane@doe.invalid')]

description = "This is the best project ever!"
name = 'myproject'
license = 'GNU GPL v3'
version = '0.0.1'

default_task = ['clean', 'analyze', 'publish']

Note that the above is equivalent to setting all the fields on project in an initializer, though the first variant above is preferred for brevity’s sake.

@init
def initialize(project):
    project.name = 'myproject'
    ...

Initializers

An initializer is a function decorated by the @init decorator. It is automatically collected by PyBuilder and executed before actual tasks are executed. The main use case of initializers is to mutate the project object in order to configure plugins.

Dependency injection

PyBuilder will automatically inject arguments by name into an initializer (provided the initializer accepts it). The arguments project and logger are available currently.

This means that all of these are fine and will work as expected:

@init
def initialize():
    pass

@init
def initialize2(logger):
    pass

@init
def initialize3(project, logger):
    pass

@init
def initialize3(logger, project):
    pass

Environments

It’s possible to execute an initializer only when a specific command-line switch was passed. This is a bit akin to Maven’s profiles:

@init(environments='myenv')
def initialize():
    pass

The above initializer will only get executed if we call pyb with the -E myenv switch.

The project object

The project object is used to describe the project and plugin settings. It also provides useful functions we can use to implement build logic.

Setting properties

PyBuilder uses a key-value based configuration for plugins. In order to set configuration, project.set_property(name, value) is used. For example we can tell the flake8 plugin to also lint our test sources with:

project.set_property('flake8_include_test_sources', True)

In some cases we just want to mutate the properties (for example adding an element to a list), this can be achieved with project.get_property(name). For example we can tell the filter_resources plugin to apply on all files named setup.cfg:

project.get_property('filter_resources_glob').append('**/setup.cfg')

Note that append mutates the list.

Project dependencies

The project object tracks our project’s dependencies. There are several variants to add dependencies:

  • project.depends_on(name) (runtime dependency)
  • project.build_depends_on(name) (build-time dependency)
  • project.depends_on(name, version) (where version is a pip version string like ‘==1.1.0’ or ‘>=1.0’)
  • project.build_depends_on(name, version) (where version is a pip version string like ‘==1.1.0’)

This will result on the install_dependencies plugin installing these dependencies when its task is called. Runtime dependencies will also be added as metadata when packaging the project, for example building a python setuptools tarball with a setup.py will fill the install_requires list.

Installing files

Installing non-python files is easily done with project.install_file(target, source). The target path may be absolute, or relative to the installation prefix (/usr/ on most linux systems).

As an important sidenote, the path to source must be relative to the distribution directory. Since non-python files are not copied to the distribution directory by default, it is necessary to use the copy_resources plugin to include them.

Consider you want to install src/main/resources/my-config.yaml in /etc/defaults. It would be done like so:

First, we use copy_resources to copy the file into the distribution directory:

use_plugin("copy_resources")

@init
def initialize(project):
    project.get_property("copy_resources_glob").append("src/main/resources/my-config.yaml")
    project.set_property("copy_resources_target", "$dir_dist")

Now, whenever copy_resources run, we will have the path src/main/resources/my-config.yaml copied into target/dist/myproject-0.0.1/src/main/resources/my-config.yaml. We’re now able to do:

use_plugin("copy_resources")

@init
def initialize(project):
    project.get_property("copy_resources_glob").append("src/main/resources/my-config.yaml")
    project.set_property("copy_resources_target", "$dir_dist")
    project.install_file("/etc/defaults", "src/main/resources/my-config.yaml")

Note

It’s important to realize that the source path src/main/resources/my-config.yaml is NOT relative to the project root directory, but relative to the distribution directory instead. It just incidentally happens to be the same here.

Including files

Simply use the include_file directive:

project.include_file(package_name, filename)

Tasks

Creating a task

To create a task, one can simply write a function in the build.py and annotate it with the @task decorator.

from pybuilder.core import task, init

@init
def initialize(project):
    pass

@task
def mytask(project, logger):
    logger.info("Hello from my task")

Like with initializer, PyBuilder will inject the arguments project and logger if the task function accepts them.

We’ll now be able to call pyb mytask.

The project API can be used to get configuration properties (so that the task is configurable). It’s also possible to compute paths by using expand_path:

from pybuilder.core import task

@task
def mytask(project, logger):
    logger.info("Will build the distribution in %s" % project.expand_path("$dir_dist"))

Task dependencies

A task can declare dependencies on other tasks by using the @depends decorator:

from pybuilder.core import task, depends

@task
def task1(logger):
    logger.info("Hello from task1")

@task
@depends("task1")
def task2(logger):
    logger.info("Hello from task2")

@task
@depends("task2", "run_unit_tests")
def task3(logger):
    logger.info("Hello from task3")

Here, running task1 will just run task1. Running task2 will run task1 first, then task2. Running task3 will run task1 first (dependency of task2), then run task2, then run unit tests, and finally run task3.