blob: a917fc3122857b67eb071f78416c62207da51296 [file] [log] [blame]
# Copyright 2019 The LUCI Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
def _version():
"""Returns a triple with lucicfg version: `(major, minor, revision)`."""
return __native__.version()
def _config(
config_service_host=None,
config_set=None,
config_dir=None,
tracked_files=None,
fail_on_warnings=None,
):
"""Sets one or more parameters for the `lucicfg` itself.
These parameters do not affect semantic meaning of generated configs, but
influence how they are generated and validated.
Each parameter has a corresponding command line flag. If the flag is present,
it overrides the value set via `lucicfg.config` (if any). For example, the
flag `-config-service-host <value>` overrides whatever was set via
`lucicfg.config(config_service_host=...)`.
`lucicfg.config` is allowed to be called multiple times. The most recently set
value is used in the end, so think of `lucicfg.config(var=...)` just as
assigning to a variable.
Args:
config_service_host: a hostname of a LUCI Config Service to send validation
requests to. Default is whatever is hardcoded in `lucicfg` binary,
usually `luci-config.appspot.com`.
config_set: name of the config set in LUCI Config Service to use for
validation. Default is `projects/<name>` where `<name>` is taken from
core.project(...) rule. If there's no such rule, the default is "",
meaning the generated config will not be validated via LUCI Config
Service.
config_dir: a directory to place generated configs into, relative to the
directory that contains the entry point \*.star file. `..` is allowed.
If set via `-config-dir` command line flag, it is relative to the
current working directory. Will be created if absent. If `-`, the
configs are just printed to stdout in a format useful for debugging.
Default is "generated".
tracked_files: a list of glob patterns that define a subset of files under
`config_dir` that are considered generated. This is important if some
generated file disappears from `lucicfg` output: it must be deleted from
the disk as well. To do this, `lucicfg` needs to know what files are
safe to delete. Each entry is either `<glob pattern>` (a "positive"
glob) or `!<glob pattern>` (a "negative" glob). A file under
`config_dir` (or any of its subdirectories) is considered tracked if it
matches any of the positive globs and none of the negative globs. For
example, `tracked_files` for prod and dev projects co-hosted in the same
directory may look like `['*.cfg', '!*-dev.cfg']` for prod and
`['*-dev.cfg']` for dev. If `tracked_files` is empty (default), lucicfg
will never delete any files. In this case it is responsibility of the
caller to make sure no stale output remains.
fail_on_warnings: if set to True treat validation warnings as errors.
Default is False (i.e. warnings do to cause the validation to fail). If
set to True via `lucicfg.config` and you want to override it to False
via command line flags use `-fail-on-warnings=false`.
"""
if config_service_host != None:
__native__.set_meta('config_service_host', config_service_host)
if config_set != None:
__native__.set_meta('config_set', config_set)
if config_dir != None:
__native__.set_meta('config_dir', config_dir)
if tracked_files != None:
__native__.set_meta('tracked_files', tracked_files)
if fail_on_warnings != None:
__native__.set_meta('fail_on_warnings', fail_on_warnings)
def _generator(impl):
"""Registers a callback that is called at the end of the config generation
stage to modify/append/delete generated configs in an arbitrary way.
The callback accepts single argument `ctx` which is a struct with the
following fields:
* **config_set**: a dict `{config file name -> (str | proto)}`.
The callback is free to modify `ctx.config_set` in whatever way it wants, e.g.
by adding new values there or mutating/deleting existing ones.
DocTags:
Advanced.
Args:
impl: a callback `func(ctx) -> None`.
"""
__native__.add_generator(impl)
def _var(default=None, validator=None):
"""Declares a variable.
A variable is a slot that can hold some frozen value. Initially this slot is
empty. lucicfg.var(...) returns a struct with methods to manipulate this slot:
* `set(value)`: sets the variable's value if it's unset, fails otherwise.
* `get()`: returns the current value, auto-setting it to `default` if it was
unset.
Note the auto-setting the value in `get()` means once `get()` is called on an
unset variable, this variable can't be changed anymore, since it becomes
initialized and initialized variables are immutable. In effect, all callers of
`get()` within a scope always observe the exact same value (either an
explicitly set one, or a default one).
Any module (loaded or exec'ed) can declare variables via lucicfg.var(...). But
only modules running through exec(...) can read and write them. Modules being
loaded via load(...) must not depend on the state of the world while they are
loading, since they may be loaded at unpredictable moments. Thus an attempt to
use `get` or `set` from a loading module causes an error.
Note that functions _exported_ by loaded modules still can do anything they
want with variables, as long as they are called from an exec-ing module. Only
code that executes _while the module is loading_ is forbidden to rely on state
of variables.
Assignments performed by an exec-ing module are visible only while this module
and all modules it execs are running. As soon as it finishes, all changes
made to variable values are "forgotten". Thus variables can be used to
implicitly propagate information down the exec call stack, but not up (use
exec's return value for that).
Generator callbacks registered via lucicfg.generator(...) are forbidden to
read or write variables, since they execute outside of context of any
exec(...). Generators must operate exclusively over state stored in the node
graph. Note that variables still can be used by functions that _build_ the
graph, they can transfer information from variables into the graph, if
necessary.
The most common application for lucicfg.var(...) is to "configure" library
modules with default values pertaining to some concrete executing script:
* A library declares variables while it loads and exposes them in its public
API either directly or via wrapping setter functions.
* An executing script uses library's public API to set variables' values to
values relating to what this script does.
* All calls made to the library from the executing script (or any scripts it
includes with exec(...)) can access variables' values now.
This is more magical but less wordy alternative to either passing specific
default values in every call to library functions, or wrapping all library
functions with wrappers that supply such defaults. These more explicit
approaches can become pretty convoluted when there are multiple scripts and
libraries involved.
DocTags:
Advanced.
Args:
default: a value to auto-set to the variable in `get()` if it was unset.
validator: a callback called as `validator(value)` from `set(value)`, must
return the value to be assigned to the variable (usually just `value`
itself).
Returns:
A struct with two methods: `set(value)` and `get(): value`.
"""
var_id = __native__.declare_var()
# The default value (if any) must pass the validation itself.
if validator and default != None:
default = validator(default)
return struct(
set = lambda v: __native__.set_var(var_id, validator(v) if validator else v),
get = lambda: __native__.get_var(var_id, default),
)
# Public API.
lucicfg = struct(
version = _version,
config = _config,
generator = _generator,
var = _var,
)