From: Lunaixsky Date: Sun, 11 May 2025 15:02:09 +0000 (+0100) Subject: refine the documentation, add extra warning messages X-Git-Url: https://scm.lunaixsky.com/lunaix-os.git/commitdiff_plain/refs/heads/eme/build-tools?ds=sidebyside;hp=9d4cc53314b8e2a236401733ac6c7093c97d4351 refine the documentation, add extra warning messages * shconfig: add "find" command to enable fuzzy searching * shconfig: use shlex.split to disassemble the command line --- diff --git a/lunaix-os/scripts/build-tools/README.lconfig.md b/lunaix-os/scripts/build-tools/README.lconfig.md index 457db72..56148e8 100644 --- a/lunaix-os/scripts/build-tools/README.lconfig.md +++ b/lunaix-os/scripts/build-tools/README.lconfig.md @@ -10,17 +10,20 @@ and allow parameterised kernel build. ## Design Motivation -LunaConfig is designed to be an improvement over the old-school Kconfig, which in -particular to address the issue of lacking hierchical representation. This is -because Kconfig organise options into a big flat list, while the options are -organised into menu which is hierachical in nature. It is very difficult to -identify membership of a config without diving into menuconfig. +LunaConfig is designed as an improvement over the old-school Kconfig, +particularly to address its lack of hierarchical representation. Kconfig +organises options into a large, flat list, even though they appear in a +hierarchical menu structure. As a result, it can be very difficult to +determine which menu a configuration option belongs to without diving into +menuconfig. -## Basic Concepts +## Basic Constructs -LunaConfig is presented as a file named `LConfig` and thus limited to one LConfig per directory. This is because LunaConfig enforce user to organise each directory to be a module or logical packaging of set of relavent functionalities. +LunaConfig is presented as a file named `LConfig` and thus limited to one +LConfig per directory. This is because LunaConfig enforce user to organise each +directory to be a module or logical packaging of set of relavent functionalities. -In each LunaConfig files, these major concepts you will usually seen. +Each LunaConfig files is comprised by these major constructs: 1. Config Component: `Terms` and `Groups` 2. Config Import @@ -44,7 +47,8 @@ value of `True`. > The logical object that is used to organise the `terms` or other `groups` -A group is similar to term but without return type indication and return statement +A group is similar to term but without return type indication and return +statement And it is usually nested with other groups or terms. @@ -54,23 +58,24 @@ def group1(): return True ``` -### LunaConfig Import +### Import Mechanism Multiple `LConfig`s may be defined across different sub-directory in large scale project for better maintainability -LunaConfig allow you to import content of other LConfig using python's relative import -feature: +LunaConfig allow you to import content of other LConfig using python's relative +import feature: ```py from . import module ``` This import mechanism works like `#include` directive in C preprocessor, -the `from . import` construct will automatically intercepted by the LConfig interpreter and -be replaced with the content from `./module/LConfig` +the `from . import` construct will be automatically intercepted by the LConfig +interpreter and be replaced with the content from `./module/LConfig` -You can also address file in the deeper hierarchy of the directory tree, for example +You can also address file in the deeper hierarchy of the directory tree, for +example ```py from .sub1.sub2 import module @@ -90,9 +95,13 @@ elif condition_2: ### Native Python -Native Python code is fully supported in LunaConfig, this include everything like packages import, functions, and class definitions. LunaConfig has the ability to distinguish these code transparently from legitimate config code. +Native Python code is fully supported in LunaConfig, this include everything +like packages import, functions, and class definitions. LunaConfig has the +ability to distinguish these code transparently from legitimate config code. -However, there is one exception for this ability. Since LunaConfig treat function definition as declaration of config component, to define a python native function you will need to decorate it with `@native`. For example: +However, there is one exception for this ability. Since LunaConfig treat +function definition as declaration of config component, to define a python +native function you will need to decorate it with `@native`. For example: ```py @native @@ -103,17 +112,22 @@ def feature1() -> int: return add(1, 2) ``` -If a native function is nested in a config component, it will not be affected by the scope and still avaliable globally. But this is not the case if it is nested by another native function. +If a native function is nested in a config component, it will not be affected +by the scope and still avaliable globally. But this is not the case if it is +nested by another native function. -If a config component is nested in a native function, then it is ignored by LunaConfig +If a config component is nested in a native function, then it is ignored by +LunaConfig ## Term Typing -A config term require it's value type to be specified explicitly. The type can be a literal type, primitive type or composite type +A config term require it's value type to be specified explicitly. The type can +be a literal type, primitive type or composite type ### Literal Typing -A term can take a literal as it's type, doing this will ensure the value taken by the term to be exactly same as the given type +A term can take a literal as it's type, doing this will ensure the value taken +by the term to be exactly same as the given type ```py # OK @@ -128,7 +142,8 @@ def feature1() -> "value": ### Primitive Typing -A term can take any python's primitive type, the value taken by the term will be type checked rather than value checked +A term can take any python's primitive type, the value taken by the term will +be type checked rather than value checked ```py # OK @@ -142,14 +157,17 @@ def feature1() -> int: ### Composite Typing -Any literal type or primitive type can be composite together via some structure to form composite type. The checking on these type is depends on the composite structure used. +Any literal type or primitive type can be composite together via some structure +to form composite type. The checking on these type is depends on the composite +structure used. #### Union Structure -A Union structure realised through binary disjunctive connector `|`. The term value must satisfy the type check against one of the composite type: +A Union structure realised through binary disjunctive connector `|`. The term +value must satisfy the type check against one of the composite type: ```py -def feature1() -> "a" | "b" | int +def feature1() -> "a" | "b" | int: # value can be either: # "a" or "b" or any integer return "a" @@ -157,15 +175,15 @@ def feature1() -> "a" | "b" | int ## Component Attributes -Each component have set of attributes to modify its behaviour and apperance, these -attributes are conveyed through decorators +Each component have set of attributes to modify its behaviour and apperance, +these attributes are conveyed through decorators ### Labels > usage: `Groups` and `Terms` -Label provide a user friendly name for a component, which will be the first choice -of the name displayed by the interactive configuration tool +Label provide a user friendly name for a component, which will be the first +choice of the name displayed by the interactive configuration tool ```py @"This is feature 1" @@ -177,7 +195,9 @@ def feature1() -> bool: > usage: `Terms` -Marking a term to be readonly prevent explicit value update, that is, manual update by user. Implicit value update initiated by `constrains` (more on this later) is still allowed. +Marking a term to be readonly prevent explicit value update, that is, manual +update by user. Implicit value update initiated by `constrains` (more on this +later) is still allowed. ```py @readonly @@ -189,9 +209,11 @@ def feature1() -> bool: > usage: `Groups` and `Terms` -A component can be marked to be hidden thus prevent it from displayed by the configuration tool, it does not affect the visibility in the code. +A component can be marked to be hidden thus prevent it from displayed by the +configuration tool, it does not affect the visibility in the code. -If the decorated target is a group, then it is inheritated by all it's subordinates. +If the decorated target is a group, then it is inherited by all it's +subordinates. ```py @hidden @@ -224,7 +246,8 @@ def feature1() -> bool: > usage: `Groups` and `Terms` -Any component can be defined outside of the logical hierachial structure (i.e., the nested function) but still attached to it's physical hierachial structure. +Any component can be defined outside of the logical hierachial structure (i.e., +the nested function) but still attached to it's physical hierachial structure. ```py @parent := parent_group @@ -236,7 +259,8 @@ def parent_group(): return False ``` -This will assigned `feature1` to be a subordinate of `parent_group`. Note that the reference to `parent_group` does not required to be after the declaration. +This will assigned `feature1` to be a subordinate of `parent_group`. Note that +the reference to `parent_group` does not required to be after the declaration. It is equivalent to @@ -253,7 +277,8 @@ def parent_group(): > usage: `Groups` and `Terms` -A help message will provide explainantion or comment of a component, to be used and displayed by the configuration tool. +A help message will provide explainantion or comment of a component, to be used +and displayed by the configuration tool. The form of message is expressed using python's doc-string @@ -274,11 +299,14 @@ There are builtin directives that is used inside the component. > usage: `Groups` and `Terms` -The dependency between components is described by various `require(...)` directives. +The dependency between components is described by various `require(...)` +directives. -This directive follows the python function call syntax and accept one argument of a boolean expression as depedency predicate. +This directive follows the python function call syntax and accept one argument +of a boolean expression as depedency predicate. -Multiple `require` directives will be chainned together with logical conjunction (i.e., `and`) +Multiple `require` directives will be chainned together with logical conjunction +(i.e., `and`) ```py def feature1() -> bool: @@ -288,19 +316,28 @@ def feature1() -> bool: return True ``` -This composition is equivalent to `feature5 and (feature2 or feature3)`, indicate that the `feature1` require presences of both `feature5` and at least one of `feature2` or `feature3`. +This composition is equivalent to `feature5 and (feature2 or feature3)`, +indicate that the `feature1` require presences of both `feature5` and at least +one of `feature2` or `feature3`. -If a dependency can not be satisfied, then the feature is disabled. This will cause it neither to be shown in configuration tool nor referencable in source code. +If a dependency can not be satisfied, then the feature is disabled. This will +cause it neither to be shown in configuration tool nor referencable in source +code. -Note that the dependency check only perform on the enablement of the node but not visibility. +Note that the dependency check only perform on the enablement of the node but +not visibility. -### Auto Toggling (Inverse Dependency) +### Constrains (Inverse Dependency) > usage: `Terms` with `bool` value type -The `when(...)` directive allow changing the default value based on the predicate evaluation on the value of other terms. Therefore, it can be only used by `Terms` with `bool` as value type. +The `when(...)` directive allow changing the default value based on the +predicate evaluation on the value of other terms. Therefore, it can be only +used by `Terms` with `bool` as value type. -Similar to `require`, it is based on the function call syntax and takes one argument of conjunctive expression (i.e., boolean expression with only `and` connectors). +Similar to `require`, it is based on the function call syntax and takes one +argument of conjunctive expression (i.e., boolean expression with only `and` +connectors). Multiple `when` will be chained together using logical disjunction (i.e., `or`) @@ -310,30 +347,35 @@ def feature1() -> bool: when (feature3) ``` -Which means `feature1` will takes value of `True` if one of the following is satisfied: +Which means `feature1` will takes value of `True` if one of the following is +satisfied: + both `feature2` and `feature5` has value of `"value1"` + `feature3` has value of True -Notice that we do not have `return` statement at the end as the precense of `when` will add a return automatically (or replace it if one exists) +Notice that we do not have `return` statement at the end as the precense of +`when` will add a return automatically (or replace it if one exists) ## Validation -For configuration language being a python superset will have a risk of abusing due to the high flexibility. This include complex logic, non-trivial operation being used in a component to derive the final value. Thus shifting the style from declarative to imperative and greatly reduce the overall reability. +Since the language is based on Python, it's very tempting to pack in advanced +features or messy logic into the config file, which can make it harder to +follow—especially as the file grows in size. -For prevention of this potential drawback, LunaConfig implemented a syntactical validator to identify these possible bad-practice and issue warning (or rise a fatal-error depending on the user setting). +LunaConfig recognises this issue and includes a built-in linter to help users +identify such bad practices. It shows warnings by default but can be configured +to raise errors instead. Currently, LunaConfig detect the misuses based on these rules: -+ `dynamic-logic`: The presence of conditional branching that could lead to complex logic. However, pattern matching is allowed. ++ `dynamic-logic`: The presence of conditional branching that could lead to + complex logic. However, pattern matching is allowed. + `while-loop`, `for-loop`: The presence of any loop structure. + `class-def`: The presence of class definition + `complex-struct`: The present of complicated data structure such as `dict`. + `side-effect`: The presence of dynamic assignment of other config terms value. -+ `non-trivial-value`: The presence of non-trivial value being used as default value. This include every things other than: - + constant - + variable reference - + comparison between constant or variable - + single boolean operation - + ternary operator with constant/refernece and trivial boolean operation ++ `non-trivial-value`: The presence of non-trivial value being used as default + value. This include every things **other than**: + + literal constant + + template literal diff --git a/lunaix-os/scripts/build-tools/lcfg2/common.py b/lunaix-os/scripts/build-tools/lcfg2/common.py index d968e67..2b0a468 100644 --- a/lunaix-os/scripts/build-tools/lcfg2/common.py +++ b/lunaix-os/scripts/build-tools/lcfg2/common.py @@ -76,7 +76,9 @@ class ValueTypeConstrain: def __parse_type(self, type): if type not in ValueTypeConstrain.TypeMap: - raise Exception(f"unknown type: {type}") + SourceLogger.warn(self.__node, self.__raw, + f"unknwon type: '{type}'. Fallback to 'str'") + return str return ValueTypeConstrain.TypeMap[type] @@ -103,7 +105,7 @@ class ValueTypeConstrain: raise node.config_error( f"unmatched type:", f"expect: '{self.schema}',", - f"got: '{val}' ({type(val)})") + f"got: '{type(val).__name__}' (val: {val})") class NodeDependency: class SimpleWalker(ast.NodeVisitor): diff --git a/lunaix-os/scripts/build-tools/lcfg2/rewriter.py b/lunaix-os/scripts/build-tools/lcfg2/rewriter.py index a86ab52..9a42447 100644 --- a/lunaix-os/scripts/build-tools/lcfg2/rewriter.py +++ b/lunaix-os/scripts/build-tools/lcfg2/rewriter.py @@ -1,6 +1,6 @@ import ast -from lib.utils import Schema +from lib.utils import Schema, SourceLogger from .lazy import Lazy from .common import NodeProperty, NodeInverseDependency @@ -139,6 +139,10 @@ class ConfigNodeASTRewriter(ast.NodeTransformer): def visit_Return(self, node): if self.__when_epxr: + SourceLogger.warn(self.__cfg_node, node, + "mixed use of `return` and `when` directive. " + "`when` have higher precedence than `return`. " + "consider remove `return` to avoid confusion") return None return self.generic_visit(node) diff --git a/lunaix-os/scripts/build-tools/lcfg2/rules.py b/lunaix-os/scripts/build-tools/lcfg2/rules.py index 3bf5683..f77460d 100644 --- a/lunaix-os/scripts/build-tools/lcfg2/rules.py +++ b/lunaix-os/scripts/build-tools/lcfg2/rules.py @@ -7,31 +7,9 @@ class SyntaxRule(RuleCollection): NodeAssigment = Schema(ast.Subscript, value=Schema(ast.Name, id='__lzLut__'), ctx=ast.Store) - TrivialValue = Schema(Schema.Union( - ast.Constant, - ast.Name - )) - BoolOperators = Schema(Schema.Union(ast.Or, ast.And)) - - TrivialTest = Schema(ast.Compare, - left=TrivialValue, - ops=[Schema.Union(ast.Eq)], - comparators=[ast.Constant]) - - InlineIf = Schema(ast.IfExp, - test=Schema.Union(TrivialTest, TrivialValue), - body=TrivialValue, - orelse=TrivialValue) - - TrivialLogic = Schema(ast.BoolOp, - op=BoolOperators, - values=Schema.List( - Schema.Union(TrivialTest, ast.Name) - )) - TrivialReturn = Schema(Schema.Union( - TrivialValue, + ast.Constant, ast.JoinedStr )) @@ -83,6 +61,6 @@ class SyntaxRule(RuleCollection): @rule(ast.Return, None, "non-trivial-value") def __nontrivial_return(self, reducer, node): """ - Option default should be kept as constant or simple membership check + Use of non-trivial value as default value """ return SyntaxRule.TrivialReturn == node.value \ No newline at end of file diff --git a/lunaix-os/scripts/build-tools/shared/shconfig/commands.py b/lunaix-os/scripts/build-tools/shared/shconfig/commands.py index 825ffa9..c668564 100644 --- a/lunaix-os/scripts/build-tools/shared/shconfig/commands.py +++ b/lunaix-os/scripts/build-tools/shared/shconfig/commands.py @@ -1,8 +1,9 @@ import textwrap import pydoc +import re from .common import CmdTable, ShconfigException -from .common import select, cmd +from .common import select, cmd, get_config_name from lcfg2.config import ConfigEnvironment from lcfg2.common import NodeProperty, NodeDependency, ConfigNodeError @@ -33,6 +34,13 @@ class Commands(CmdTable): f"{select(hidden, 'h', '.')}" \ val_txt = f"{value if value is not None else ''}" + + if value is True: + val_txt = "y" + elif value is False: + val_txt = "n" + elif isinstance(value, str): + val_txt = f'"{val_txt}"' line = f"[{status}] {name}" to_pad = max(aligned - len(line), 4) @@ -42,6 +50,24 @@ class Commands(CmdTable): line = f"\x1b[90;49m{line}\x1b[0m" return line + def __format_config_list(self, nodes): + lines = [] + disabled = [] + + for node in nodes: + _l = disabled if not NodeProperty.Enabled[node] else lines + _l.append(self.__get_opt_line(node, True)) + + if disabled: + lines += [ + "", + "\t---- disabled ----", + "", + *disabled + ] + + return lines + @cmd("help", "h") def __fn_help(self): """ @@ -68,13 +94,16 @@ class Commands(CmdTable): " r Read-Only config", " h Hidden config", "", + " VALUE (bool)", + " y True", + " n False", + "", "", "Defined configuration terms", "" ] - for node in self.__env.terms(): - lines.append(self.__get_opt_line(node, True)) + lines += self.__format_config_list(self.__env.terms()) pydoc.pager("\n".join(lines)) @@ -130,7 +159,7 @@ class Commands(CmdTable): node = self.__get_node(name) print(self.__get_opt_line(node)) - @cmd("what", "?") + @cmd("what", "help", "?") def __fn_what(self, name: str): """ Show the documentation associated with the option @@ -145,8 +174,8 @@ class Commands(CmdTable): print() - @cmd("effect", "link") - def __fn_effect(self, name: str): + @cmd("affects") + def __fn_affect(self, name: str): """ Show the effects of this option on other options """ @@ -162,4 +191,25 @@ class Commands(CmdTable): for expr in exprs: print(f" > when {expr}") print() - \ No newline at end of file + + @cmd("find") + def __fn_search(self, fuzz: str): + """ + Perform fuzzy search on configs (accept regex) + """ + + nodes = [] + expr = re.compile(fuzz) + for node in self.__env.terms(): + name = get_config_name(node._name) + if not expr.findall(name): + continue + nodes.append(node) + + if not nodes: + print("no matches") + return + + lines = self.__format_config_list(nodes) + + pydoc.pager("\n".join(lines)) \ No newline at end of file diff --git a/lunaix-os/scripts/build-tools/shared/shconfig/main.py b/lunaix-os/scripts/build-tools/shared/shconfig/main.py index 5e7c916..216e8e6 100644 --- a/lunaix-os/scripts/build-tools/shared/shconfig/main.py +++ b/lunaix-os/scripts/build-tools/shared/shconfig/main.py @@ -1,5 +1,6 @@ import readline, textwrap +from shlex import split as shsplit from rlcompleter import Completer from lcfg2.config import ConfigEnvironment from .common import ShconfigException, get_config_name @@ -30,7 +31,7 @@ def next_input(cmds: Commands): if len(line) == 0: return True - parts = line.split(' ') + parts = shsplit(line) name, args = parts[0], parts[1:] if name in ['q', 'exit']: @@ -76,5 +77,4 @@ def shconfig(env: ConfigEnvironment): except KeyboardInterrupt as e: return False except Exception as e: - raise e - return False + raise e \ No newline at end of file