Skip to main content

Package-local values

This RFC proposes to extend buck2 Starlark with package-local values.

Why

DevX people want to have some per-directory configuration files, accessible from Starlark macros.

For example, a project NNN may want to switch to building using LLVM 15 by default. End users would want to have an easy instruction how to do that, after DevX people provided instructions and infrastructure for that.

What we have now

Currently, in fbcode, we have get_modes mechanism.

get_modes symbol is registered in per-package implicit symbols, here.

This symbol can be accessed from macros using implicit_package_symbol function.

get_modes functions are package-local, but all BUILD_MODE.bzl files need to be registered in global buckconfig, which is not ideal.

Proposed per-package properties can replace get_modes mechanism.

API

PACKAGE files

Before evaluating BUCK file, buck2 will evaluate all PACKAGE files in the same directory and all parent directories. Absent PACKAGE files are treated as empty files.

All relevant PACKAGE files are executed sequentially from the root directory to the current directory (but unrelated PACKAGE files can be executed in parallel). Evaluating PACKAGE files sequentially provides additional guarantees, for example, attempt to override a property (unless explicitly requested) should fail with Starlark call stack.

Each PACKAGE file is evaluated at most once (like bzl file).

PACKAGE files may load arbitrary bzl files. BUCK-specific functions called in bzl files (like rule functions) are available, but calling functions from PACKAGE files is an error. This way, bzl files are evaluated only once regardless of whether they are loaded from PACKAGE or BUCK file.

API

PACKAGE files have a global function:

PACKAGE file API

def write_package_value(
name: str,
value: "",
overwrite: bool = False,
): ...

Name is a string which must contain exactly one dot symbol (just to enforce code style).

Value is an arbitrary Starlark value, for example, an integer, a list of integer, a struct or a function.

When overwrite is False (default), attempt to overwrite per-package value defined in parent PACKAGE file will fail.

Written values are frozen when PACKAGE file evaluation is finished.

Note write_package_value symbol exists in bzl globals, and it can be called from bzl file in context of PACKAGE evaluation, but calling write_package_file is an error on context of BUCK evaluation.

Modifying PACKAGE file logically invalidates the BUCK file of this package, and all PACKAGE and BUCK files of subpackages. However, BUCK file evaluation may track which package-local values were accessed and only invalidate BUCK files which were potentially affected (similarly to how we do it with buckconfigs, with individual properties being projection keys).

BUCK file API

BUCK files (and bzl files included from BUCK files) have a global function:

def read_package_value(
name: str,
): ...

This function returns the nearest value registered per package, or None is such value does not exist.

This function is available in bzl files, but attempt to call this function in context of PACKAGE file evaluation results in an error. This restriction can be lifted in the future.

Per-package values are not accessible as global symbols in BUCK files. We may reconsider it in the future.

read_config

PACKAGE files may call read_config function.