go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/lucicfg.star (about) 1 # Copyright 2019 The LUCI Authors. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """Core lucicfg-related functions.""" 16 17 load("@stdlib//internal/error.star", "error") 18 load("@stdlib//internal/strutil.star", "strutil") 19 20 def _version(): 21 """Returns a triple with lucicfg version: `(major, minor, revision)`.""" 22 return __native__.version 23 24 def _check_version(min, message = None): 25 """Fails if lucicfg version is below the requested minimal one. 26 27 Useful when a script depends on some lucicfg feature that may not be 28 available in earlier versions. lucicfg.check_version(...) can be used at 29 the start of the script to fail right away with a clean error message: 30 31 ```python 32 lucicfg.check_version( 33 min = "1.30.14", 34 message = "Update depot_tools", 35 ) 36 ``` 37 38 Or even 39 40 ```python 41 lucicfg.check_version("1.30.14") 42 ``` 43 44 Additionally implicitly auto-enables not-yet-default lucicfg functionality 45 released with the given version. That way lucicfg changes can be gradually 46 rolled out project-by-project by bumping the version string passed to 47 lucicfg.check_version(...) in project configs. 48 49 Args: 50 min: a string `major.minor.revision` with minimally accepted version. 51 Required. 52 message: a custom failure message to show. 53 """ 54 min_ver = strutil.parse_version(min) 55 cur_ver = _version() 56 if cur_ver < min_ver: 57 fail( 58 "Your lucicfg version v%s is older than required v%s. %s." % ( 59 "%d.%d.%d" % cur_ver, 60 "%d.%d.%d" % min_ver, 61 message or "Please update", 62 ), 63 ) 64 __native__.set_min_version_for_experiments(min_ver) 65 66 def _config( 67 *, 68 config_service_host = None, 69 config_dir = None, 70 tracked_files = None, 71 fail_on_warnings = None, 72 lint_checks = None): 73 r"""Sets one or more parameters for the `lucicfg` itself. 74 75 These parameters do not affect semantic meaning of generated configs, but 76 influence how they are generated and validated. 77 78 Each parameter has a corresponding command line flag. If the flag is 79 present, it overrides the value set via `lucicfg.config` (if any). For 80 example, the flag `-config-service-host <value>` overrides whatever was set 81 via `lucicfg.config(config_service_host=...)`. 82 83 `lucicfg.config` is allowed to be called multiple times. The most recently 84 set value is used in the end, so think of `lucicfg.config(var=...)` just as 85 assigning to a variable. 86 87 Args: 88 config_service_host: a hostname of a LUCI Config Service to send 89 validation requests to. Default is whatever is hardcoded in `lucicfg` 90 binary, usually `config.luci.app`. 91 config_dir: a directory to place generated configs into, relative to the 92 directory that contains the entry point \*.star file. `..` is allowed. 93 If set via `-config-dir` command line flag, it is relative to the 94 current working directory. Will be created if absent. If `-`, the 95 configs are just printed to stdout in a format useful for debugging. 96 Default is "generated". 97 tracked_files: a list of glob patterns that define a subset of files under 98 `config_dir` that are considered generated. Each entry is either 99 `<glob pattern>` (a "positive" glob) or `!<glob pattern>` (a "negative" 100 glob). A file under `config_dir` is considered tracked if its 101 slash-separated path matches any of the positive globs and none of the 102 negative globs. If a pattern starts with `**/`, the rest of it is 103 applied to the base name of the file (not the whole path). If only 104 negative globs are given, single positive `**/*` glob is implied as 105 well. `tracked_files` can be used to limit what files are actually 106 emitted: if this set is not empty, only files that are in this set will 107 be actually written to the disk (and all other files are discarded). 108 This is beneficial when `lucicfg` is used to generate only a subset of 109 config files, e.g. during the migration from handcrafted to generated 110 configs. Knowing the tracked files set is also important when some 111 generated file disappears from `lucicfg` output: it must be deleted from 112 the disk as well. To do this, `lucicfg` needs to know what files are 113 safe to delete. If `tracked_files` is empty (default), `lucicfg` will 114 save all generated files and will never delete any file in this case it 115 is responsibility of the caller to make sure no stale output remains). 116 fail_on_warnings: if set to True treat validation warnings as errors. 117 Default is False (i.e. warnings do not cause the validation to fail). 118 If set to True via `lucicfg.config` and you want to override it to False 119 via command line flags use `-fail-on-warnings=false`. 120 lint_checks: a list of linter checks to apply in `lucicfg validate`. The 121 first entry defines what group of checks to use as a base and it can 122 be one of `none`, `default` or `all`. The following entries either 123 add checks to the set (`+<name>`) or remove them (`-<name>`). See 124 [Formatting and linting Starlark code](#formatting-linting) for more 125 info. Default is `['none']` for now. 126 """ 127 if config_service_host != None: 128 __native__.set_meta("config_service_host", config_service_host) 129 if config_dir != None: 130 __native__.set_meta("config_dir", config_dir) 131 if tracked_files != None: 132 __native__.set_meta("tracked_files", tracked_files) 133 if fail_on_warnings != None: 134 __native__.set_meta("fail_on_warnings", fail_on_warnings) 135 if lint_checks != None: 136 __native__.set_meta("lint_checks", lint_checks) 137 138 def _enable_experiment(experiment): 139 """Enables an experimental feature. 140 141 Can be used to experiment with non-default features that may later 142 change in a non-backwards compatible way or even be removed completely. 143 Primarily intended for lucicfg developers to test their features before they 144 are "frozen" to be backward compatible. 145 146 Enabling an experiment that doesn't exist logs a warning, but doesn't fail 147 the execution. Refer to the documentation and the source code for the list 148 of available experiments. 149 150 Args: 151 experiment: a string ID of the experimental feature to enable. Required. 152 """ 153 __native__.enable_experiment(experiment) 154 155 def _generator(impl): 156 """Registers a generator callback. 157 158 Such callback is called at the end of the config generation stage to 159 modify/append/delete generated configs in an arbitrary way. 160 161 The callback accepts single argument `ctx` which is a struct with the 162 following fields and methods: 163 164 * **output**: a dict `{config file name -> (str | proto)}`. The callback 165 is free to modify `ctx.output` in whatever way it wants, e.g. by adding 166 new values there or mutating/deleting existing ones. 167 168 * **declare_config_set(name, root)**: proclaims that generated configs 169 under the given root (relative to `config_dir`) belong to the given 170 config set. Safe to call multiple times with exact same arguments, but 171 changing an existing root to something else is an error. 172 173 DocTags: 174 Advanced. 175 176 Args: 177 impl: a callback `func(ctx) -> None`. 178 """ 179 __native__.add_generator(impl) 180 181 def _emit(*, dest = None, data = None): 182 """Tells lucicfg to write given data to some output file. 183 184 In particular useful in conjunction with io.read_file(...) to copy files 185 into the generated output: 186 187 ```python 188 lucicfg.emit( 189 dest = "foo.cfg", 190 data = io.read_file("//foo.cfg"), 191 ) 192 ``` 193 194 Note that lucicfg.emit(...) cannot be used to override generated files. 195 `dest` must refer to a path not generated or emitted by anything else. 196 197 Args: 198 dest: path to the output file, relative to the `config_dir` (see 199 lucicfg.config(...)). Must not start with `../`. Required. 200 data: either a string or a proto message to write to `dest`. Proto 201 messages are serialized using text protobuf encoding. Required. 202 """ 203 trace = stacktrace(skip = 2) 204 205 def _emit_data(ctx): 206 _, err = __native__.clean_relative_path("", dest, False) 207 if err: 208 error("%s", err, trace = trace) 209 return 210 if ctx.output.get(dest) != None: 211 error("config file %r is already generated by something else", dest, trace = trace) 212 return 213 ctx.output[dest] = data 214 215 _generator(impl = _emit_data) 216 217 def _current_module(): 218 """Returns the location of a module being currently executed. 219 220 This is the module being processed by a current load(...) or exec(...) 221 statement. It has no relation to the module that holds the top-level stack 222 frame. For example, if a currently loading module `A` calls a function in 223 a module `B` and this function calls lucicfg.current_module(...), the result 224 would be the module `A`, even though the call goes through code in the 225 module `B` (i.e. lucicfg.current_module(...) invocation itself resided in 226 a function in module `B`). 227 228 Fails if called from inside a generator callback. Threads executing such 229 callbacks are not running any load(...) or exec(...). 230 231 Returns: 232 A `struct(package='...', path='...')` with the location of the module. 233 """ 234 pkg, path = __native__.current_module() 235 return struct(package = pkg, path = path) 236 237 # A constructor for lucicfg.var structs. 238 _var_ctor = __native__.genstruct("lucicfg.var") 239 240 def _var(*, default = None, validator = None, expose_as = None): 241 """Declares a variable. 242 243 A variable is a slot that can hold some frozen value. Initially this slot is 244 usually empty. lucicfg.var(...) returns a struct with methods to manipulate 245 it: 246 247 * `set(value)`: sets the variable's value if it's unset, fails otherwise. 248 * `get()`: returns the current value, auto-setting it to `default` if it 249 was unset. 250 251 Note the auto-setting the value in `get()` means once `get()` is called on 252 an unset variable, this variable can't be changed anymore, since it becomes 253 initialized and initialized variables are immutable. In effect, all callers 254 of `get()` within a scope always observe the exact same value (either an 255 explicitly set one, or a default one). 256 257 Any module (loaded or exec'ed) can declare variables via lucicfg.var(...). 258 But only modules running through exec(...) can read and write them. Modules 259 being loaded via load(...) must not depend on the state of the world while 260 they are loading, since they may be loaded at unpredictable moments. Thus 261 an attempt to use `get` or `set` from a loading module causes an error. 262 263 Note that functions _exported_ by loaded modules still can do anything they 264 want with variables, as long as they are called from an exec-ing module. 265 Only code that executes _while the module is loading_ is forbidden to rely 266 on state of variables. 267 268 Assignments performed by an exec-ing module are visible only while this 269 module and all modules it execs are running. As soon as it finishes, all 270 changes made to variable values are "forgotten". Thus variables can be used 271 to implicitly propagate information down the exec call stack, but not up 272 (use exec's return value for that). 273 274 Generator callbacks registered via lucicfg.generator(...) are forbidden to 275 read or write variables, since they execute outside of context of any 276 exec(...). Generators must operate exclusively over state stored in the node 277 graph. Note that variables still can be used by functions that _build_ the 278 graph, they can transfer information from variables into the graph, if 279 necessary. 280 281 The most common application for lucicfg.var(...) is to "configure" library 282 modules with default values pertaining to some concrete executing script: 283 284 * A library declares variables while it loads and exposes them in its 285 public API either directly or via wrapping setter functions. 286 * An executing script uses library's public API to set variables' values 287 to values relating to what this script does. 288 * All calls made to the library from the executing script (or any scripts 289 it includes with exec(...)) can access variables' values now. 290 291 This is more magical but less wordy alternative to either passing specific 292 default values in every call to library functions, or wrapping all library 293 functions with wrappers that supply such defaults. These more explicit 294 approaches can become pretty convoluted when there are multiple scripts and 295 libraries involved. 296 297 Another use case is to allow parameterizing configs with values passed via 298 CLI flags. A string-typed var can be declared with `expose_as=<name>` 299 argument, making it settable via `-var <name>=<value>` CLI flag. This is 300 primarily useful in conjunction with `-emit-to-stdout` CLI flag to use 301 lucicfg as a "function call" that accepts arguments via CLI flags and 302 returns the result via stdout to pipe somewhere else, e.g. 303 304 ```shell 305 lucicfg generate main.star -var environ=dev -emit-to-stdout all.json | ... 306 ``` 307 308 **Danger**: Using `-var` without `-emit-to-stdout` is generally wrong, since 309 configs generated on disk (and presumably committed into a repository) must 310 not depend on undetermined values passed via CLI flags. 311 312 DocTags: 313 Advanced. 314 315 Args: 316 default: a value to auto-set to the variable in `get()` if it was unset. 317 validator: a callback called as `validator(value)` from `set(value)` and 318 inside lucicfg.var(...) declaration itself (to validate `default` or a 319 value passed via CLI flags). Must be a side-effect free idempotent 320 function that returns the value to be assigned to the variable (usually 321 just `value` itself, but conversions are allowed, including type 322 changes). 323 expose_as: an optional string identifier to make this var settable via 324 CLI flags as `-var <expose_as>=<value>`. If there's no such flag, the 325 variable is auto-initialized to its default value (which must be string 326 or None). Variables declared with `expose_as` are not settable via 327 `set()` at all, they appear as "set" already the moment they are 328 declared. If multiple vars use the same `expose_as` identifier, they 329 will all be initialized to the same value. 330 331 Returns: 332 A struct with two methods: `set(value)` and `get(): value`. 333 """ 334 335 # Variables that can be bound to CLI flags are string-value, and thus the 336 # default value must also be a string (or be absent). 337 if expose_as and not (default == None or type(default) == "string"): 338 fail( 339 "lucicfg.var declared with expose_as must have a string or None " + 340 "default, got %s %s" % (type(default), default), 341 ) 342 343 # The default value (if any) must pass the validation itself. 344 if validator and default != None: 345 default = validator(default) 346 347 # Validate the value passed via CLI flag (if any). 348 preset_value = None 349 if expose_as: 350 preset_value = __native__.var_flags.get(expose_as) 351 if preset_value == None: 352 preset_value = default 353 elif validator: 354 preset_value = validator(preset_value) 355 356 # This declares the variable and pre-sets it to validated value passed via 357 # CLI flags (if expose_as is not None). This also puts the corresponding 358 # -var flag in the set of "consumed" flags. At the end of the script 359 # execution all -var flags provided on the command line must be consumed 360 # (the run fails otherwise). 361 var_id = __native__.declare_var(expose_as or "", preset_value) 362 363 return _var_ctor( 364 set = lambda v: __native__.set_var(var_id, validator(v) if validator else v), 365 get = lambda: __native__.get_var(var_id, default), 366 ) 367 368 def _rule(*, impl, defaults = None): 369 """Declares a new rule. 370 371 A rule is a callable that adds nodes and edges to an entity graph. It wraps 372 the given `impl` callback by passing one additional argument `ctx` to it (as 373 the first positional argument). 374 375 `ctx` is a struct with the following fields: 376 377 * `defaults`: a struct with module-scoped defaults for the rule. 378 379 The callback is expected to return a graph.keyset(...) with the set of graph 380 keys that represent the added node (or nodes). Other rules use such keysets 381 as inputs. 382 383 DocTags: 384 Advanced. RuleCtor. 385 386 Args: 387 impl: a callback that actually implements the rule. Its first argument 388 should be `ctx`. The rest of the arguments define the API of the rule. 389 Required. 390 defaults: a dict with keys matching the rule arguments and values of type 391 lucicfg.var(...). These variables can be used to set defaults to use for 392 a rule within some exec scope (see lucicfg.var(...) for more details 393 about scoping). These vars become the public API of the rule. Callers 394 can set them via `rule.defaults.<name>.set(...)`. `impl` callback can 395 get them via `ctx.defaults.<name>.get()`. It is up to the rule's author 396 to define vars for fields that can have defaults, document them in the 397 rule doc, and finally use them from `impl` callback. 398 399 Returns: 400 A special callable. 401 """ 402 return __native__.declare_rule(impl, defaults or {}) 403 404 # Public API. 405 406 lucicfg = struct( 407 version = _version, 408 check_version = _check_version, 409 config = _config, 410 enable_experiment = _enable_experiment, 411 generator = _generator, 412 emit = _emit, 413 current_module = _current_module, 414 var = _var, 415 rule = _rule, 416 )