You've already forked pgbackrest
mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2026-10-23 21:28:56 +03:00
Most of these are prose in a python docstring or comment wrapped a few characters wide, which black does not reflow, and macro continuation backslashes left stranded past the margin. The rest are long calls broken after the first parenthesis, section separator comments with too many dashes, and long expectation strings split along what they hold. A few long shell commands in the workflows are wrapped with continuations. hrnTestResultDiff() had a continuation backslash left over from when it was a macro, which spliced the closing brace onto the line above it. It is removed. The linter that enforces the limit follows, so this commit is only the reformatting and can be ignored by blame.
710 lines
29 KiB
Python
710 lines
29 KiB
Python
"""Parse Configuration Yaml.
|
|
|
|
Reads the command and option declarations, which are the definition of record for the pgBackRest command line and configuration
|
|
file.
|
|
|
|
Most of the work here is inheritance. An option can take another option's command list, allow list, or dependency, and a command
|
|
entry in an option can default to what the option itself declares, so what a declaration means depends on what was declared before
|
|
it. That is resolved here rather than at render time, so the render sees complete options.
|
|
|
|
The declaration order is also what the reader sees, so an option can only inherit from an option above it."""
|
|
|
|
####################################################################################################################################
|
|
import copy
|
|
import os
|
|
|
|
from common.error import ToolError, check
|
|
from common.storage import file_read
|
|
from common.yaml import YamlMap, yaml_bool, yaml_load, yaml_map_empty
|
|
|
|
# Command roles. A role is a process that carries out part of a command, and main is the process the user started. The order here is
|
|
# the order the roles are rendered in.
|
|
CMD_ROLE_MAIN = "main"
|
|
CMD_ROLE_ASYNC = "async"
|
|
CMD_ROLE_LOCAL = "local"
|
|
CMD_ROLE_REMOTE = "remote"
|
|
|
|
CMD_ROLE_LIST = (CMD_ROLE_MAIN, CMD_ROLE_ASYNC, CMD_ROLE_LOCAL, CMD_ROLE_REMOTE)
|
|
|
|
# Commands that take no options of their own, so they are left out of an option's command list
|
|
CMD_HELP = "help"
|
|
CMD_VERSION = "version"
|
|
|
|
# Option types
|
|
OPT_TYPE_BOOLEAN = "boolean"
|
|
OPT_TYPE_HASH = "hash"
|
|
OPT_TYPE_INTEGER = "integer"
|
|
OPT_TYPE_LIST = "list"
|
|
OPT_TYPE_PATH = "path"
|
|
OPT_TYPE_SIZE = "size"
|
|
OPT_TYPE_STRING = "string"
|
|
OPT_TYPE_STRING_ID = "string-id"
|
|
OPT_TYPE_TIME = "time"
|
|
|
|
# Options that the rest of the build refers to by name
|
|
OPT_STANZA = "stanza"
|
|
|
|
# Sections an option can be configured in. An option in the command-line section cannot be set in the configuration file, which is
|
|
# why it can be neither negated nor reset.
|
|
SECTION_COMMAND_LINE = "command-line"
|
|
|
|
# How a default value is rendered
|
|
DEFAULT_TYPE_QUOTE = "quote"
|
|
DEFAULT_TYPE_LITERAL = "literal"
|
|
DEFAULT_TYPE_DYNAMIC = "dynamic"
|
|
|
|
_DEFAULT_TYPE_LIST = (DEFAULT_TYPE_QUOTE, DEFAULT_TYPE_LITERAL, DEFAULT_TYPE_DYNAMIC)
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgCommand:
|
|
"""A command."""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.internal = False # Is the command internal?
|
|
self.log_file = True # Does the command write automatically to a log file?
|
|
self.log_level_default = "info" # Default log level
|
|
self.lock_required = False # Is a lock required?
|
|
self.lock_remote_required = False # Is a remote lock required?
|
|
self.lock_type = "none" # Lock type
|
|
self.parameter_allowed = False # Are command line parameters allowed?
|
|
self.role_list = None # Roles valid for the command
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionGroup:
|
|
"""An option group, i.e. a set of options that can be indexed, e.g. repo1-path and repo2-path."""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionValue:
|
|
"""A value in an allow list, which may be compiled in only when a feature is."""
|
|
|
|
def __init__(self, value, condition):
|
|
self.value = value
|
|
self.condition = condition # Is the value conditionally compiled?
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionAllowRangeMap:
|
|
"""An allowed range for one value of the option the range maps on."""
|
|
|
|
def __init__(self, map, min, max):
|
|
self.map = map
|
|
self.min = min
|
|
self.max = max
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionAllowRange:
|
|
"""An allowed range, either one range or a range per value of another option."""
|
|
|
|
def __init__(self, min=None, max=None, map_list=None):
|
|
self.min = min
|
|
self.max = max
|
|
self.map_list = map_list
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionDefaultMap:
|
|
"""A default for one value of the option the default maps on."""
|
|
|
|
def __init__(self, map, value):
|
|
self.map = map
|
|
self.value = value
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionDefault:
|
|
"""A default value, either one value or a value per value of another option."""
|
|
|
|
def __init__(self, value=None, map_list=None):
|
|
self.value = value
|
|
self.map_list = map_list
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionDepend:
|
|
"""A dependency on another option."""
|
|
|
|
def __init__(self, option, default_value, value_list):
|
|
self.option = option # Option the dependency is on
|
|
self.default_value = default_value # Value to use when the dependency is not resolved
|
|
self.value_list = value_list # Values of the option that resolve the dependency
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionDeprecate:
|
|
"""A deprecated name for an option."""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.indexed = False # Can the deprecation be indexed?
|
|
self.unindexed = False # Can the deprecation be unindexed?
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOptionCommand:
|
|
"""What an option means for one command."""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.internal = None # Is the option internal, or None to use what the option declares?
|
|
self.required = None # Is the option required, or None to use what the option declares?
|
|
self.sequence = None # Is a sequence added to the StringId, or None to use what the option declares?
|
|
self.default_value = None
|
|
self.depend = None
|
|
self.allow_list = None
|
|
self.role_list = None # Roles of the command the option is valid for
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfgOption:
|
|
"""An option."""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.type = None # Option type, e.g. integer
|
|
self.section = None # Section the option can be configured in
|
|
self.bool_like = False # Does the option accept y/n and can be treated as a boolean?
|
|
self.internal = False # Is the option internal?
|
|
self.beta = False # Is the option beta?
|
|
self.required = True # Is the option required?
|
|
self.negate = None # Can the option be negated?
|
|
self.reset = False # Can the option be reset?
|
|
self.sequence = False # Is a sequence added to the StringId?
|
|
self.default_type = DEFAULT_TYPE_QUOTE # How the default is rendered
|
|
self.default_value = None
|
|
self.group = None # Option group, if any
|
|
self.secure = False # Does the option contain a secret?
|
|
self.depend = None
|
|
self.allow_list = None
|
|
self.allow_range = None
|
|
self.cmd_list = None # What the option means for each command it is valid for
|
|
self.cmd_role_list = None # Roles the option is valid for, before they are filtered per command
|
|
self.deprecate_list = None
|
|
|
|
|
|
####################################################################################################################################
|
|
class BldCfg:
|
|
"""The configuration declaration."""
|
|
|
|
def __init__(self, cmd_list, opt_grp_list, opt_list, opt_resolve_list):
|
|
self.cmd_list = cmd_list # Commands, sorted
|
|
self.opt_grp_list = opt_grp_list # Option groups, sorted
|
|
self.opt_list = opt_list # Options, sorted
|
|
self.opt_resolve_list = opt_resolve_list # Options in the order their dependencies can be resolved in
|
|
|
|
|
|
####################################################################################################################################
|
|
def _find(item_list, name):
|
|
"""Find an entry in a list by name, or None when it is not there."""
|
|
|
|
for item in item_list:
|
|
if item.name == name:
|
|
return item
|
|
|
|
return None
|
|
|
|
|
|
####################################################################################################################################
|
|
def _command_role(role_raw, name):
|
|
"""Parse a command role list, which is a map of roles with nothing under them."""
|
|
|
|
result = []
|
|
|
|
for role, detail in role_raw:
|
|
yaml_map_empty(detail, "%s role '%s'" % (name, role))
|
|
result.append(role)
|
|
|
|
return result
|
|
|
|
|
|
####################################################################################################################################
|
|
def _command_list(cmd_raw):
|
|
"""Parse the command list."""
|
|
|
|
result = []
|
|
|
|
for name, detail in cmd_raw:
|
|
cmd = BldCfgCommand(name)
|
|
|
|
for key, value in detail:
|
|
if key == "command-role":
|
|
cmd.role_list = _command_role(value, "command '%s'" % name)
|
|
elif key == "internal":
|
|
cmd.internal = yaml_bool(value, "command '%s' internal" % name)
|
|
elif key == "lock-type":
|
|
cmd.lock_type = value
|
|
elif key == "lock-remote-required":
|
|
cmd.lock_remote_required = yaml_bool(value, "command '%s' lock-remote-required" % name)
|
|
elif key == "lock-required":
|
|
cmd.lock_required = yaml_bool(value, "command '%s' lock-required" % name)
|
|
elif key == "log-file":
|
|
cmd.log_file = yaml_bool(value, "command '%s' log-file" % name)
|
|
elif key == "log-level-default":
|
|
cmd.log_level_default = value.lower()
|
|
elif key == "parameter-allowed":
|
|
cmd.parameter_allowed = yaml_bool(value, "command '%s' parameter-allowed" % name)
|
|
else:
|
|
raise ToolError("unknown command definition '%s'" % key)
|
|
|
|
# Every command has a main role whether it declares one or not, since main is the process the user started
|
|
cmd.role_list = sorted(set(cmd.role_list or []) | {CMD_ROLE_MAIN})
|
|
|
|
result.append(cmd)
|
|
|
|
return sorted(result, key=lambda cmd: cmd.name)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _option_group_list(opt_grp_raw):
|
|
"""Parse the option group list, which is a map of groups with nothing under them."""
|
|
|
|
result = []
|
|
|
|
for name, detail in opt_grp_raw:
|
|
yaml_map_empty(detail, "option group '%s'" % name)
|
|
result.append(BldCfgOptionGroup(name))
|
|
|
|
return sorted(result, key=lambda opt_grp: opt_grp.name)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _allow_list(allow_raw, opt_list, name):
|
|
"""Parse an allow list, which is either the list of allowed values or the name of an option to take it from."""
|
|
|
|
# A scalar names the option the allow list is inherited from
|
|
if isinstance(allow_raw, str):
|
|
check(opt_list is not None, "allow list for %s cannot be inherited" % name)
|
|
|
|
opt_inherit = _find(opt_list, allow_raw)
|
|
check(opt_inherit is not None, "allow list inherited from option '%s' before it is defined" % allow_raw)
|
|
|
|
return copy.copy(opt_inherit.allow_list)
|
|
|
|
result = []
|
|
|
|
for value in allow_raw:
|
|
# A scalar is the value on its own, else a map of the value to the feature it is compiled in with
|
|
if isinstance(value, str):
|
|
result.append(BldCfgOptionValue(value, None))
|
|
else:
|
|
check(len(value) == 1, "allow list value for %s must have a single condition" % name)
|
|
|
|
for item, condition in value:
|
|
result.append(BldCfgOptionValue(item, condition))
|
|
|
|
return result
|
|
|
|
|
|
####################################################################################################################################
|
|
def _allow_range(range_raw, name):
|
|
"""Parse an allow range, which is either a min/max pair or a min/max pair per value of the option it maps on."""
|
|
|
|
# A scalar first means the range itself rather than a range per map
|
|
if isinstance(range_raw[0], str):
|
|
check(len(range_raw) == 2, "allow range for %s must be a min and a max" % name)
|
|
|
|
return BldCfgOptionAllowRange(min=range_raw[0], max=range_raw[1])
|
|
|
|
map_list = []
|
|
|
|
for entry in range_raw:
|
|
for map, range in entry:
|
|
check(len(range) == 2, "allow range for %s map '%s' must be a min and a max" % (name, map))
|
|
map_list.append(BldCfgOptionAllowRangeMap(map, range[0], range[1]))
|
|
|
|
return BldCfgOptionAllowRange(map_list=map_list)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _default(default_raw):
|
|
"""Parse a default, which is either the value or a value per value of the option it maps on.
|
|
|
|
A tilde overrides an inherited default back to no default at all."""
|
|
|
|
# A scalar is the default itself
|
|
if isinstance(default_raw, str):
|
|
return None if default_raw == "~" else BldCfgOptionDefault(value=default_raw)
|
|
|
|
map_list = []
|
|
|
|
for entry in default_raw:
|
|
for map, value in entry:
|
|
map_list.append(BldCfgOptionDefaultMap(map, value))
|
|
|
|
return BldCfgOptionDefault(map_list=map_list)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _depend(depend_raw, opt_list):
|
|
"""Parse a dependency, which is either the dependency or the name of an option to take it from."""
|
|
|
|
# A scalar names the option the dependency is inherited from
|
|
if isinstance(depend_raw, str):
|
|
opt_inherit = _find(opt_list, depend_raw)
|
|
|
|
if opt_inherit is None:
|
|
raise ToolError("dependency inherited from option '%s' before it is defined" % depend_raw)
|
|
|
|
return opt_inherit.depend
|
|
|
|
option = None
|
|
default_value = None
|
|
value_list = None
|
|
|
|
for key, value in depend_raw:
|
|
if key == "list":
|
|
value_list = list(value)
|
|
elif key == "default":
|
|
default_value = value
|
|
elif key == "option":
|
|
option = value
|
|
else:
|
|
raise ToolError("unknown depend definition '%s'" % key)
|
|
|
|
return BldCfgOptionDepend(option, default_value, value_list)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _depend_reconcile(opt, depend, opt_list):
|
|
"""Resolve a dependency to the option it is on, now that every option exists."""
|
|
|
|
if depend is None:
|
|
return None
|
|
|
|
if depend.default_value is not None and opt.type not in (OPT_TYPE_BOOLEAN, OPT_TYPE_INTEGER):
|
|
raise ToolError("dependency default invalid for non integer/boolean option '%s'" % opt.name)
|
|
|
|
opt_depend = _find(opt_list, depend.option)
|
|
|
|
if opt_depend is None:
|
|
raise ToolError("dependency on undefined option '%s'" % depend.option)
|
|
|
|
return BldCfgOptionDepend(opt_depend, depend.default_value, depend.value_list)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _deprecate_list(deprecate_raw):
|
|
"""Parse the deprecated names of an option.
|
|
|
|
A question mark in the name means the deprecation may be indexed, e.g. repo?-path covers repo-path and repo1-path, so the same
|
|
name can appear twice and each spelling is recorded separately."""
|
|
|
|
result = []
|
|
|
|
for name, detail in deprecate_raw:
|
|
yaml_map_empty(detail, "deprecate '%s'" % name)
|
|
|
|
indexed = "?" in name
|
|
name = name.replace("?", "", 1)
|
|
|
|
deprecate = _find(result, name)
|
|
|
|
if deprecate is None:
|
|
deprecate = BldCfgOptionDeprecate(name)
|
|
result.append(deprecate)
|
|
|
|
if indexed:
|
|
deprecate.indexed = True
|
|
else:
|
|
deprecate.unindexed = True
|
|
|
|
return sorted(result, key=lambda deprecate: deprecate.name)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _option_command_list(opt_cmd_raw, cmd_list, opt_list, name):
|
|
"""Parse the commands an option is valid for, which is either the list or the name of an option to take it from."""
|
|
|
|
# A scalar names the option the command list is inherited from
|
|
if isinstance(opt_cmd_raw, str):
|
|
opt_inherit = _find(opt_list, opt_cmd_raw)
|
|
check(opt_inherit is not None, "command list inherited from option '%s' before it is defined" % opt_cmd_raw)
|
|
|
|
return opt_inherit.cmd_list
|
|
|
|
result = []
|
|
|
|
for opt_cmd, detail in opt_cmd_raw:
|
|
# Add every command of another option, stripped of what that option declared about them
|
|
if opt_cmd == "+inherit":
|
|
opt_inherit = _find(opt_list, detail)
|
|
check(opt_inherit is not None, "commands inherited from option '%s' before it is defined" % detail)
|
|
|
|
for cmd in opt_inherit.cmd_list:
|
|
result.append(BldCfgOptionCommand(cmd.name))
|
|
# Add every command that has a role, or any role
|
|
elif opt_cmd == "+role":
|
|
for cmd in cmd_list:
|
|
# Help and version take no options of their own
|
|
if cmd.name in (CMD_HELP, CMD_VERSION):
|
|
continue
|
|
|
|
for role in cmd.role_list:
|
|
if (detail == "any" or role == detail) and _find(result, cmd.name) is None:
|
|
result.append(BldCfgOptionCommand(cmd.name))
|
|
# Remove a command, which is how a command is trimmed from what +inherit or +role added
|
|
elif opt_cmd == "-command":
|
|
cmd = _find(result, detail)
|
|
|
|
if cmd is not None:
|
|
result.remove(cmd)
|
|
# Else the command is declared with what it overrides
|
|
else:
|
|
cmd = BldCfgOptionCommand(opt_cmd)
|
|
|
|
for key, value in detail:
|
|
if key == "allow-list":
|
|
cmd.allow_list = _allow_list(value, None, "%s command '%s'" % (name, opt_cmd))
|
|
elif key == "command-role":
|
|
cmd.role_list = _command_role(value, "%s command '%s'" % (name, opt_cmd))
|
|
elif key == "depend":
|
|
cmd.depend = _depend(value, opt_list)
|
|
elif key == "default":
|
|
cmd.default_value = _default(value)
|
|
elif key == "internal":
|
|
cmd.internal = yaml_bool(value, "%s command '%s' internal" % (name, opt_cmd))
|
|
elif key == "required":
|
|
cmd.required = yaml_bool(value, "%s command '%s' required" % (name, opt_cmd))
|
|
elif key == "sequence":
|
|
cmd.sequence = yaml_bool(value, "%s command '%s' sequence" % (name, opt_cmd))
|
|
else:
|
|
raise ToolError("unknown option command definition '%s'" % key)
|
|
|
|
# A declaration replaces whatever +inherit or +role added for the command
|
|
cmd_prior = _find(result, opt_cmd)
|
|
|
|
if cmd_prior is not None:
|
|
result.remove(cmd_prior)
|
|
|
|
result.append(cmd)
|
|
|
|
return sorted(result, key=lambda cmd: cmd.name)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _option_list_raw(opt_raw, cmd_list, opt_grp_list):
|
|
"""Parse the options as declared, before any of them are resolved against each other."""
|
|
|
|
result = []
|
|
|
|
for name, detail in opt_raw:
|
|
opt = BldCfgOption(name)
|
|
inherit_found = False
|
|
|
|
for key, value in detail:
|
|
if key == "allow-list":
|
|
opt.allow_list = _allow_list(value, result, "option '%s'" % name)
|
|
elif key == "allow-range":
|
|
opt.allow_range = _allow_range(value, "option '%s'" % name)
|
|
elif key == "command":
|
|
opt.cmd_list = _option_command_list(value, cmd_list, result, "option '%s'" % name)
|
|
elif key == "command-role":
|
|
opt.cmd_role_list = _command_role(value, "option '%s'" % name)
|
|
elif key == "default":
|
|
opt.default_value = _default(value)
|
|
elif key == "depend":
|
|
opt.depend = _depend(value, result)
|
|
elif key == "deprecate":
|
|
opt.deprecate_list = _deprecate_list(value)
|
|
elif key == "default-type":
|
|
check(value in _DEFAULT_TYPE_LIST, "option '%s' has invalid default type '%s'" % (name, value))
|
|
opt.default_type = value
|
|
elif key == "group":
|
|
check(_find(opt_grp_list, value) is not None, "option '%s' has invalid group '%s'" % (name, value))
|
|
opt.group = value
|
|
elif key == "inherit":
|
|
opt_inherit = _find(result, value)
|
|
check(opt_inherit is not None, "option '%s' inherited before it is defined" % value)
|
|
|
|
# Everything declared before inherit is replaced and everything after it overrides, which is why inherit is written
|
|
# first. Deprecations are not inherited since they name the option they are a deprecation of.
|
|
opt = copy.copy(opt_inherit)
|
|
opt.name = name
|
|
opt.deprecate_list = None
|
|
|
|
inherit_found = True
|
|
elif key == "internal":
|
|
opt.internal = yaml_bool(value, "option '%s' internal" % name)
|
|
elif key == "bool-like":
|
|
opt.bool_like = yaml_bool(value, "option '%s' bool-like" % name)
|
|
elif key == "beta":
|
|
opt.beta = yaml_bool(value, "option '%s' beta" % name)
|
|
elif key == "negate":
|
|
opt.negate = yaml_bool(value, "option '%s' negate" % name)
|
|
elif key == "sequence":
|
|
opt.sequence = yaml_bool(value, "option '%s' sequence" % name)
|
|
elif key == "required":
|
|
opt.required = yaml_bool(value, "option '%s' required" % name)
|
|
elif key == "section":
|
|
opt.section = value
|
|
elif key == "secure":
|
|
opt.secure = yaml_bool(value, "option '%s' secure" % name)
|
|
elif key == "type":
|
|
opt.type = value
|
|
else:
|
|
raise ToolError("unknown option definition '%s'" % key)
|
|
|
|
if opt.type is None:
|
|
raise ToolError("option '%s' requires 'type'" % name)
|
|
|
|
# An inherited option already has these set from the option it inherited from
|
|
if not inherit_found:
|
|
# An option that does not say where it can be configured can only be given on the command line
|
|
if opt.section is None:
|
|
opt.section = SECTION_COMMAND_LINE
|
|
|
|
# Only an option that can be written in the configuration file can be negated there
|
|
if opt.negate is None:
|
|
opt.negate = (opt.type == OPT_TYPE_BOOLEAN or opt.bool_like) and opt.section != SECTION_COMMAND_LINE
|
|
|
|
# An option that does not name its commands is valid for all of them
|
|
if opt.cmd_list is None:
|
|
opt.cmd_list = sorted(
|
|
(BldCfgOptionCommand(cmd.name) for cmd in cmd_list if cmd.name not in (CMD_HELP, CMD_VERSION)),
|
|
key=lambda cmd: cmd.name,
|
|
)
|
|
|
|
# Only an option that can be written in the configuration file can be reset, which is what removes it again
|
|
opt.reset = opt.section != SECTION_COMMAND_LINE
|
|
|
|
result.append(opt)
|
|
|
|
return sorted(result, key=lambda opt: opt.name)
|
|
|
|
|
|
####################################################################################################################################
|
|
def _option_list(opt_raw, cmd_list, opt_grp_list):
|
|
"""Parse the option list and resolve every option against the others."""
|
|
|
|
opt_list_raw = _option_list_raw(opt_raw, cmd_list, opt_grp_list)
|
|
|
|
# Copy the options so a dependency can point at the option it is on rather than at the name of one
|
|
result = []
|
|
|
|
for opt_raw in opt_list_raw:
|
|
opt = copy.copy(opt_raw)
|
|
opt.cmd_list = None
|
|
opt.depend = None
|
|
result.append(opt)
|
|
|
|
# Resolve what each option means for each of its commands, which is what it declares for the command falling back to what it
|
|
# declares for itself
|
|
for idx, opt_raw in enumerate(opt_list_raw):
|
|
cmd_opt_list = []
|
|
|
|
for opt_cmd_raw in opt_raw.cmd_list:
|
|
cmd = _find(cmd_list, opt_cmd_raw.name)
|
|
|
|
if cmd is None:
|
|
raise ToolError("invalid command '%s' in option '%s' command list" % (opt_cmd_raw.name, opt_raw.name))
|
|
|
|
opt_cmd = copy.copy(opt_cmd_raw)
|
|
|
|
if opt_cmd.required is None:
|
|
opt_cmd.required = opt_raw.required
|
|
|
|
if opt_cmd.internal is None:
|
|
opt_cmd.internal = opt_raw.internal
|
|
|
|
if opt_cmd.sequence is None:
|
|
opt_cmd.sequence = opt_raw.sequence
|
|
|
|
if opt_cmd.role_list is None:
|
|
# What the option declares for every command, less the roles this command does not have
|
|
if opt_raw.cmd_role_list is not None:
|
|
opt_cmd.role_list = [role for role in opt_raw.cmd_role_list if role in cmd.role_list]
|
|
else:
|
|
opt_cmd.role_list = cmd.role_list
|
|
|
|
opt_cmd.depend = _depend_reconcile(opt_raw, opt_cmd_raw.depend, result)
|
|
cmd_opt_list.append(opt_cmd)
|
|
|
|
result[idx].cmd_list = cmd_opt_list
|
|
result[idx].depend = _depend_reconcile(opt_raw, opt_raw.depend, result)
|
|
|
|
return result
|
|
|
|
|
|
####################################################################################################################################
|
|
def _option_resolve_list(opt_list):
|
|
"""Order the options so that every option comes after the options it depends on.
|
|
|
|
The stanza option is resolved first since an error about a missing option is confusing when the stanza it would apply to is what
|
|
is actually missing, so stanza must exist and may not depend on anything."""
|
|
|
|
opt_stanza = _find(opt_list, OPT_STANZA)
|
|
|
|
if opt_stanza is None:
|
|
raise ToolError("option '%s' must exist" % OPT_STANZA)
|
|
|
|
if opt_stanza.depend is not None:
|
|
raise ToolError("option '%s' may not depend on other option" % OPT_STANZA)
|
|
|
|
for opt_cmd in opt_stanza.cmd_list:
|
|
if opt_cmd.depend is not None:
|
|
raise ToolError("option '%s' command '%s' may not depend on other option" % (OPT_STANZA, opt_cmd.name))
|
|
|
|
resolve_list = [OPT_STANZA]
|
|
|
|
while len(resolve_list) != len(opt_list):
|
|
resolved = False
|
|
|
|
for opt in opt_list:
|
|
if opt.name in resolve_list:
|
|
continue
|
|
|
|
if opt.depend is not None and opt.depend.option.name not in resolve_list:
|
|
continue
|
|
|
|
if any(cmd.depend is not None and cmd.depend.option.name not in resolve_list for cmd in opt.cmd_list):
|
|
continue
|
|
|
|
resolve_list.append(opt.name)
|
|
resolved = True
|
|
|
|
# Nothing resolved means the options left depend on each other, since every other reason to skip is already excluded
|
|
if not resolved:
|
|
raise ToolError(
|
|
"unable to resolve dependencies for option(s) '%s'\nHINT: are there circular dependencies?"
|
|
% ", ".join(opt.name for opt in opt_list if opt.name not in resolve_list)
|
|
)
|
|
|
|
return [_find(opt_list, name) for name in resolve_list]
|
|
|
|
|
|
####################################################################################################################################
|
|
def bld_cfg_parse(path_repo):
|
|
"""Parse config.yaml into the configuration declaration."""
|
|
|
|
path = os.path.join(path_repo, "build/config.yaml")
|
|
cfg_raw = yaml_load(file_read(path), path)
|
|
|
|
cmd_raw = None
|
|
opt_grp_raw = None
|
|
opt_raw = None
|
|
|
|
for key, value in cfg_raw:
|
|
if key == "command":
|
|
cmd_raw = value
|
|
elif key == "optionGroup":
|
|
opt_grp_raw = value
|
|
elif key == "option":
|
|
opt_raw = value
|
|
else:
|
|
raise ToolError("unknown config definition '%s'" % key)
|
|
|
|
check(cmd_raw is not None and opt_grp_raw is not None and opt_raw is not None, "command, optionGroup, and option are required")
|
|
|
|
cmd_list = _command_list(cmd_raw)
|
|
opt_grp_list = _option_group_list(opt_grp_raw)
|
|
opt_list = _option_list(opt_raw, cmd_list, opt_grp_list)
|
|
|
|
return BldCfg(cmd_list, opt_grp_list, opt_list, _option_resolve_list(opt_list))
|