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  )