From 0857be16339a6110b3f3f17529cf96dc2a1bf849 Mon Sep 17 00:00:00 2001 From: Egg12138 Date: Mon, 8 Dec 2025 18:21:00 +0800 Subject: [PATCH 1/3] neo-generate: update feat parsing logic - neo-generate unexpected filtered out other fileds of config scope in .yaml - rename "nightly-features"-based symbols to more general naming Signed-off-by: egg12138 --- .../app/plugins/neo_generate/neo_generate.py | 7 +++-- src/oebuild/nightly_features.py | 30 +++++++++++++++---- src/oebuild/parse_template.py | 13 +++++++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/oebuild/app/plugins/neo_generate/neo_generate.py b/src/oebuild/app/plugins/neo_generate/neo_generate.py index 22d3513..571b7c0 100644 --- a/src/oebuild/app/plugins/neo_generate/neo_generate.py +++ b/src/oebuild/app/plugins/neo_generate/neo_generate.py @@ -235,7 +235,7 @@ class NeoGenerate(OebuildCommand): yocto_oebuild_dir=yocto_oebuild_dir, parser_template=parser_template, ) - self._add_nightly_features_template( + self._add_feat_template( parser_template, resolved_features ) return parser_template @@ -298,7 +298,7 @@ class NeoGenerate(OebuildCommand): sys.exit(-1) - def _add_nightly_features_template( + def _add_feat_template( self, parser_template: ParseTemplate, resolved_features ): for feature in resolved_features: @@ -320,6 +320,9 @@ class NeoGenerate(OebuildCommand): if local_conf is None else LiteralScalarString(local_conf), support=feature.machines or [], + other_configs=feature.config.other_fields + if feature.config.other_fields + else None, ) ) diff --git a/src/oebuild/nightly_features.py b/src/oebuild/nightly_features.py index 1ce6852..d0e7b6b 100644 --- a/src/oebuild/nightly_features.py +++ b/src/oebuild/nightly_features.py @@ -38,9 +38,12 @@ class AmbiguourError(ResolutionError): @dataclass class FeatureConfig: + # frequently used fields repos: List[str] = field(default_factory=list) layers: List[str] = field(default_factory=list) local_conf: List[str] = field(default_factory=list) + # Store any additional config fields + other_fields: Dict[str, any] = field(default_factory=dict) @dataclass @@ -226,14 +229,31 @@ class FeatureRegistry: parent.child_full_ids.append(feature.full_id) def _parse_config(self, config_block: Optional[dict]) -> FeatureConfig: + """ + _parse_config() will normalize three most frequently used fields: + * repos + * layers + * local_conf + + Fourtunetly, for other fields, which are all defined as a simple k:v map, hence just load them directly + """ if not isinstance(config_block, dict): config_block = {} + + repos = self._normalize_sequence(config_block.get('repos')) + layers = self._normalize_sequence(config_block.get('layers')) + local_conf = self._normalize_local_conf(config_block.get('local_conf')) + + other_fields = {} + for key, value in config_block.items(): + if key not in ('repos', 'layers', 'local_conf'): + other_fields[key] = value + return FeatureConfig( - repos=self._normalize_sequence(config_block.get('repos')), - layers=self._normalize_sequence(config_block.get('layers')), - local_conf=self._normalize_local_conf( - config_block.get('local_conf') - ), + repos=repos, + layers=layers, + local_conf=local_conf, + other_fields=other_fields, ) def _parse_machines( diff --git a/src/oebuild/parse_template.py b/src/oebuild/parse_template.py index 29f8ebb..267497e 100644 --- a/src/oebuild/parse_template.py +++ b/src/oebuild/parse_template.py @@ -13,7 +13,7 @@ See the Mulan PSL v2 for more details. import logging import sys from dataclasses import dataclass -from typing import Optional +from typing import Optional, Any, Dict import pathlib import os @@ -57,6 +57,8 @@ class FeatureTemplate(Template): support: list + other_configs: Optional[Dict[str, str]] = None + class BaseParseTemplate(ValueError): """ @@ -279,6 +281,7 @@ class ParseTemplate: compile_conf = self._deal_non_essential_compile_conf_param( param, compile_conf ) + self._apply_feat_configs(compile_conf) compile_conf['no_layer'] = param['no_layer'] compile_conf['repos'] = repos compile_conf['local_conf'] = local_conf @@ -317,6 +320,14 @@ class ParseTemplate: compile_conf['tmp_dir'] = param['tmp_dir'] return compile_conf + def _apply_feat_configs(self, compile_conf): + for feature in self.feature_template: + extra = getattr(feature, 'other_configs', None) + if not extra: + continue + for key, value in extra.items(): + compile_conf[key] = value + @staticmethod def parse_repos_list(repos): """ -- Gitee From 86c18707ef2e02aea8301d182dde16b45fd508f7 Mon Sep 17 00:00:00 2001 From: Egg12138 Date: Tue, 16 Dec 2025 19:58:59 +0800 Subject: [PATCH 2/3] refactor(neo-menuconfig): add common options and optimize design - Extend MenuconfigSelection dataclass to include build environment and common build options like sstate mirrors, toolchain paths, and temporary directories. Just like oebuild generate used to do. - Add precomputed data structures for faster symbol lookup and dependency resolution. - Implement dependency-aware feature hierarchy with recursion depth protection. - Add common options block for build configuration with conditional dependencies based on build environment choice. - Improve error handling and cleanup. Signed-off-by: egg12138 --- .../neo_generate/menuconfig_generator.py | 355 +++++++++++++++--- .../app/plugins/neo_generate/neo_generate.py | 23 +- 2 files changed, 323 insertions(+), 55 deletions(-) diff --git a/src/oebuild/app/plugins/neo_generate/menuconfig_generator.py b/src/oebuild/app/plugins/neo_generate/menuconfig_generator.py index 0b387a7..0afa6a0 100644 --- a/src/oebuild/app/plugins/neo_generate/menuconfig_generator.py +++ b/src/oebuild/app/plugins/neo_generate/menuconfig_generator.py @@ -7,7 +7,9 @@ from __future__ import annotations import os import re import tempfile -from collections import OrderedDict +import textwrap +import warnings +from collections import OrderedDict, defaultdict from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path @@ -16,6 +18,7 @@ from typing import Dict, Generator, List, Optional from kconfiglib import Kconfig from menuconfig import menuconfig +import oebuild.const as oebuild_const import oebuild.util as oebuild_util from oebuild.nightly_features import Feature, FeatureRegistry @@ -30,12 +33,66 @@ class MenuconfigSelection: features: List[str] """Full feature IDs that ended up enabled.""" + build_in: str + """Resolved build environment (docker/host).""" + + no_fetch: bool + """Whether source fetching is disabled.""" + + no_layer: bool + """Whether layer updates are skipped.""" + + sstate_mirrors: Optional[str] + """Optional SSTATE_MIRRORS override.""" + + sstate_dir: Optional[str] + """Optional SSTATE_DIR override.""" + + tmp_dir: Optional[str] + """Optional TMPDIR path for host builds.""" + + toolchain_dir: Optional[str] + """Optional external GCC toolchain path.""" + + llvm_toolchain_dir: Optional[str] + """Optional external LLVM toolchain path.""" + + nativesdk_dir: Optional[str] + """Optional nativesdk root path for host builds.""" + + datetime: Optional[str] + """Optional DATETIME value for local.conf.""" + + cache_src_dir: Optional[str] + """Optional cache_src_dir path override.""" + + directory: Optional[str] + """Optional build directory name override.""" + class NeoMenuconfigGenerator: """Builds a nightly-feature menuconfig that mirrors the catalog hierarchy.""" PLATFORM_PREFIX = 'PLATFORM_' FEATURE_PREFIX = 'FEATURE_' + MAX_RECURSION_DEPTH = 20 + BUILD_IN_CHOICES = ( + ('BUILD_IN-DOCKER', oebuild_const.BUILD_IN_DOCKER), + ('BUILD_IN-HOST', oebuild_const.BUILD_IN_HOST), + ) + COMMON_STRING_SYMBOLS = OrderedDict( + [ + ('COMMON_SSTATE-MIRRORS', 'sstate_mirrors'), + ('COMMON_SSTATE-DIR', 'sstate_dir'), + ('COMMON_TMP-DIR', 'tmp_dir'), + ('COMMON_TOOLCHAIN-DIR', 'toolchain_dir'), + ('COMMON_LLVM-TOOLCHAIN-DIR', 'llvm_toolchain_dir'), + ('COMMON_NATIVESDK-DIR', 'nativesdk_dir'), + ('COMMON_DATETIME', 'datetime'), + ('COMMON_CACHE_SRC_DIR', 'cache_src_dir'), + ('COMMON_DIRECTORY', 'directory'), + ] + ) def __init__( self, @@ -57,7 +114,42 @@ class NeoMenuconfigGenerator: ) self.platform_symbol_map: Dict[str, str] = OrderedDict() self.feature_symbol_map: Dict[str, str] = OrderedDict() - self._emitted_features: set[str] = set() + self._dependency_children = self._build_dependency_children() + self._dependency_child_ids = { + child.full_id + for children in self._dependency_children.values() + for child in children + } + # Precompute sorted data for all features + self._sorted_deps_map: Dict[str, List[str]] = {} + self._sorted_selects_map: Dict[str, List[str]] = {} + self._sorted_one_of_map: Dict[str, List[str]] = {} + self._sorted_choice_map: Dict[str, List[str]] = {} + self._sorted_child_ids_map: Dict[str, List[str]] = {} + self._feature_to_symbol_map: Dict[str, str] = {} + self._platform_to_symbol_map: Dict[str, str] = {} + + # Precompute feature data + for feature in self.registry.features_by_full_id.values(): + full_id = feature.full_id + # Symbol mapping (forward and reverse) + normalized = re.sub(r'[^A-Z0-9]', '_', full_id.upper()) + symbol = f'{self.FEATURE_PREFIX}{normalized}' + self._feature_to_symbol_map[full_id] = symbol + self.feature_symbol_map[symbol] = full_id + # Sorted lists + self._sorted_deps_map[full_id] = sorted(set(feature.dependencies)) + self._sorted_selects_map[full_id] = sorted(set(feature.selects)) + self._sorted_one_of_map[full_id] = sorted(feature.one_of) + self._sorted_choice_map[full_id] = sorted(feature.choice) + self._sorted_child_ids_map[full_id] = sorted(feature.child_full_ids) + + # Precompute platform symbol mappings (forward and reverse) + for machine in self.platforms: + normalized = re.sub(r'[^A-Z0-9]', '_', machine.upper()) + symbol = f'{self.PLATFORM_PREFIX}{normalized}' + self._platform_to_symbol_map[machine] = symbol + self.platform_symbol_map[symbol] = machine def run_menuconfig(self) -> Optional[MenuconfigSelection]: """Generate a Kconfig, run menuconfig, and translate the selections.""" @@ -67,7 +159,9 @@ class NeoMenuconfigGenerator: kconfig_path.write_text(kconfig_text, encoding='utf-8') kconf = Kconfig(str(kconfig_path)) previous_style = os.environ.get('MENUCONFIG_STYLE') - os.environ['MENUCONFIG_STYLE'] = 'aquatic selection=fg:white,bg:blue' + os.environ['MENUCONFIG_STYLE'] = ( + 'aquatic selection=fg:white,bg:blue' + ) with self._hook_write_config() as saved_filename: try: @@ -86,8 +180,8 @@ class NeoMenuconfigGenerator: try: Path(saved_filename[0]).unlink() - except OSError: - pass + except OSError as e: + warnings.warn(f"Failed to delete temporary config file {saved_filename[0]}: {e}") return selection @@ -119,15 +213,16 @@ class NeoMenuconfigGenerator: def build_kconfig_text(self) -> str: """Return the textual Kconfig representation without launching menuconfig.""" - self._emitted_features.clear() lines: List[str] = [ '# Auto-generated nightly-feature menuconfig', '# Updating this file manually is not supported.', '', ] - lines += self._platform_block() + lines += self._platform_choice_block() lines.append('') lines += self._features_block() + lines.append('\n') + lines += self._common_options_block() return '\n'.join(lines) def _list_platforms(self) -> List[str]: @@ -140,9 +235,8 @@ class NeoMenuconfigGenerator: result.append(entry.stem) return result - def _platform_block(self) -> List[str]: - block = ['menu "Platform"'] - block.append('choice') + def _platform_choice_block(self) -> List[str]: + block = ['choice'] block.append(' prompt "Select target platform"') default_symbol = self._symbol_for_platform(self.default_platform) block.append(f' default {default_symbol}') @@ -152,15 +246,74 @@ class NeoMenuconfigGenerator: block.append(f' bool "{machine}"') self.platform_symbol_map[symbol] = machine block.append('endchoice') - block.append('endmenu') return block + def _common_options_block(self) -> List[str]: + block = textwrap.dedent(""" + choice + prompt "Select build environment" + default BUILD_IN-DOCKER + config BUILD_IN-DOCKER + bool "docker" + config BUILD_IN-HOST + bool "host" + endchoice + + config COMMON_NO-FETCH + bool "no_fetch (disable source fetching)" + default n + + config COMMON_NO-LAYER + bool "no_layer (skip layer repo update on env setup)" + default n + + config COMMON_SSTATE-MIRRORS + string "SSTATE_MIRRORS value" + default "" + + config COMMON_SSTATE-DIR + string "SSTATE_DIR path" + default "" + + config COMMON_TMP-DIR + string "TMPDIR path" + default "" + depends on BUILD_IN-HOST + + config COMMON_TOOLCHAIN-DIR + string "toolchain_dir (External GCC toolchain directory [your own toolchain])" + default "" + + config COMMON_LLVM-TOOLCHAIN-DIR + string "llvm_toolchain_dir (External LLVM toolchain directory [your own toolchain])" + default "" + + config COMMON_NATIVESDK-DIR + string "nativesdk_dir (External nativesdk directory [used when building on host])" + default "" + depends on BUILD_IN-HOST + + config COMMON_DATETIME + string "datetime" + default "" + + config COMMON_CACHE_SRC_DIR + string "cache_src_dir (src directory)" + default "" + + config COMMON_DIRECTORY + string "directory (build directory name)" + default "" + """) + return [line for line in block.strip().splitlines()] + def _features_block(self) -> List[str]: - lines: List[str] = ['menu "Nightly Features"'] + lines: List[str] = ['menu "Select Features"'] + emitted_features: set[str] = set() for category, features in self._root_features_by_category(): lines.append(f'menu "{self._format_category_label(category)}"') for feature in features: - lines += self._emit_feature_block(feature, indent=1) + lines += self._emit_feature_block(feature, 1, emitted_features) lines.append('endmenu') lines.append('') lines.append('endmenu') @@ -171,6 +324,8 @@ class NeoMenuconfigGenerator: for feature in self.registry.features_by_full_id.values(): if feature.is_subfeature: continue + if feature.full_id in self._dependency_child_ids: + continue grouped.setdefault(feature.category, []).append(feature) sorted_groups = [] for category in sorted(grouped): @@ -185,14 +340,44 @@ class NeoMenuconfigGenerator: ) return sorted_groups + def _build_dependency_children(self) -> Dict[str, List[Feature]]: + # Build mapping from dependency ID to features that depend on it + dep_to_features: Dict[str, List[Feature]] = defaultdict(list) + for feature in self.registry.features_by_full_id.values(): + if feature.is_subfeature: + continue + for dep_id in feature.dependencies: + dep_to_features[dep_id].append(feature) + + # Only keep category roots that are dependencies + result: Dict[str, List[Feature]] = defaultdict(list) + for parent in self.registry.category_roots.values(): + if parent.full_id in dep_to_features: + # Filter features that belong to same category + for feature in dep_to_features[parent.full_id]: + if feature.full_id == parent.full_id: + continue + if feature.category != parent.category: + continue + result[parent.full_id].append(feature) + # Sort by full_id for deterministic output + result[parent.full_id].sort(key=lambda feat: feat.full_id) + return result + def _emit_feature_block( self, feature: Feature, indent: int, + emitted_features: set[str], ) -> List[str]: - if feature.full_id in self._emitted_features: + if indent > self.MAX_RECURSION_DEPTH: + raise RuntimeError( + f"Recursion depth exceeded for feature {feature.full_id}. " + f"Maximum allowed depth is {self.MAX_RECURSION_DEPTH}." + ) + if feature.full_id in emitted_features: return [] - self._emitted_features.add(feature.full_id) + emitted_features.add(feature.full_id) lines: List[str] = [] prefix = ' ' * indent symbol = self._symbol_for_feature(feature.full_id) @@ -206,75 +391,91 @@ class NeoMenuconfigGenerator: depends_expr = self._build_dependency_expression(feature) if depends_expr: lines.append(f'{prefix} depends on {depends_expr}') - selects = sorted(set(feature.selects)) + dependencies = self._sorted_dependencies(feature.full_id) + for dependency_id in dependencies: + lines.append( + f'{prefix} select {self._symbol_for_feature(dependency_id)}' + ) + selects = self._sorted_selects(feature.full_id) for selects_id in selects: lines.append( f'{prefix} select {self._symbol_for_feature(selects_id)}' ) lines.append('') - lines += self._emit_subfeature_sections(feature, indent + 1) + lines += self._emit_subfeature_sections(feature, indent, emitted_features) return lines def _emit_subfeature_sections( - self, feature: Feature, indent: int + self, feature: Feature, indent: int, emitted_features: set[str] ) -> List[str]: - lines: List[str] = [] - prefix = ' ' * indent - one_of_children = sorted(feature.one_of) + child_lines: List[str] = [] + child_prefix = ' ' * (indent + 1) + one_of_children = self._sorted_one_of(feature.full_id) if one_of_children: - lines.append(f'{prefix}choice') - lines.append( - f'{prefix} prompt "Select mode for {feature.name}"' + child_lines.append(f'{child_prefix}choice') + child_lines.append( + f'{child_prefix} prompt "Select mode for {feature.name}"' ) - lines.append( - f'{prefix} depends on {self._symbol_for_feature(feature.full_id)}' + child_lines.append( + f'{child_prefix} depends on {self._symbol_for_feature(feature.full_id)}' ) if feature.default_one_of: - lines.append( - f'{prefix} default {self._symbol_for_feature(feature.default_one_of)}' + child_lines.append( + f'{child_prefix} default {self._symbol_for_feature(feature.default_one_of)}' ) - lines.append('') + child_lines.append('') for child_id in one_of_children: child = self.registry.features_by_full_id.get(child_id) if child is None: continue - lines += self._emit_feature_block(child, indent + 1) - lines.append(f'{prefix}endchoice') - lines.append('') - choice_children = sorted(feature.choice) + child_lines += self._emit_feature_block(child, indent + 1, emitted_features) + child_lines.append(f'{child_prefix}endchoice') + child_lines.append('') + choice_children = self._sorted_choice(feature.full_id) if choice_children: - lines.append(f'{prefix}menu "Optional {feature.name} add-ons"') + child_lines.append( + f'{child_prefix}menu "Optional {feature.name} add-ons"' + ) for child_id in choice_children: child = self.registry.features_by_full_id.get(child_id) if child is None: continue - lines += self._emit_feature_block(child, indent + 1) - lines.append(f'{prefix}endmenu') - lines.append('') + child_lines += self._emit_feature_block(child, indent + 1, emitted_features) + child_lines.append(f'{child_prefix}endmenu') + child_lines.append('') + sorted_child_ids = self._sorted_child_ids(feature.full_id) remaining_children = [ child_id - for child_id in feature.child_full_ids + for child_id in sorted_child_ids if child_id not in one_of_children and child_id not in choice_children ] - for child_id in sorted(remaining_children): + for child_id in remaining_children: child = self.registry.features_by_full_id.get(child_id) if child is None: continue - lines += self._emit_feature_block(child, indent + 1) - return lines + child_lines += self._emit_feature_block(child, indent + 1, emitted_features) + for child in self._dependency_children.get(feature.full_id, []): + child_lines += self._emit_feature_block(child, indent + 1, emitted_features) + if not child_lines: + return [] + prefix = ' ' * indent + return [ + f'{prefix}if {self._symbol_for_feature(feature.full_id)}', + *child_lines, + f'{prefix}endif', + '', + ] def _build_help(self, feature: Feature) -> List[str]: help_lines: List[str] = [] if feature.prompt: help_lines += feature.prompt.strip().splitlines() if feature.machines: - help_lines.append( - f'Supports: {", ".join(feature.machines)}' - ) + help_lines.append(f'Supports: {", ".join(feature.machines)}') if feature.dependencies: help_lines.append( - 'Depends on: ' + ', '.join(sorted(feature.dependencies)) + 'Depends on: ' + ', '.join(self._sorted_dependencies(feature.full_id)) ) return help_lines @@ -282,8 +483,6 @@ class NeoMenuconfigGenerator: terms: List[str] = [] if feature.parent_full_id: terms.append(self._symbol_for_feature(feature.parent_full_id)) - for dependency in sorted(set(feature.dependencies)): - terms.append(self._symbol_for_feature(dependency)) machine_expr = self._build_machine_expression(feature) if machine_expr: if terms: @@ -316,19 +515,71 @@ class NeoMenuconfigGenerator: selected_features.append(full_id) if selected_platform is None: selected_platform = self.default_platform + + def bool_option(symbol_name: str) -> bool: + sym = syms.get(symbol_name) + return bool(sym and sym.str_value == 'y') + + def string_option(symbol_name: str) -> Optional[str]: + sym = syms.get(symbol_name) + if sym is None or sym.str_value is None: + return None + normalized = sym.str_value.strip() + if not normalized or normalized.lower() == 'none': + return None + return normalized + + build_in = oebuild_const.BUILD_IN_DOCKER + for symbol_name, env_value in self.BUILD_IN_CHOICES: + if bool_option(symbol_name): + build_in = env_value + break + + string_values = { + attr_name: string_option(symbol_name) + for symbol_name, attr_name in self.COMMON_STRING_SYMBOLS.items() + } + return MenuconfigSelection( platform=selected_platform, features=selected_features, + build_in=build_in, + no_fetch=bool_option('COMMON_NO-FETCH'), + no_layer=bool_option('COMMON_NO-LAYER'), + **string_values, ) + def _sorted_dependencies(self, full_id: str) -> List[str]: + return self._sorted_deps_map.get(full_id, []) + + def _sorted_selects(self, full_id: str) -> List[str]: + return self._sorted_selects_map.get(full_id, []) + + def _sorted_one_of(self, full_id: str) -> List[str]: + return self._sorted_one_of_map.get(full_id, []) + + def _sorted_choice(self, full_id: str) -> List[str]: + return self._sorted_choice_map.get(full_id, []) + + def _sorted_child_ids(self, full_id: str) -> List[str]: + return self._sorted_child_ids_map.get(full_id, []) + def _symbol_for_feature(self, full_id: str) -> str: - normalized = re.sub(r'[^A-Z0-9]', '_', full_id.upper()) - return f'{self.FEATURE_PREFIX}{normalized}' + symbol = self._feature_to_symbol_map.get(full_id) + if symbol is None: + # Fallback calculation (should not happen if precomputed) + normalized = re.sub(r'[^A-Z0-9]', '_', full_id.upper()) + symbol = f'{self.FEATURE_PREFIX}{normalized}' + self._feature_to_symbol_map[full_id] = symbol + return symbol def _symbol_for_platform(self, machine: str) -> str: - normalized = re.sub(r'[^A-Z0-9]', '_', machine.upper()) - symbol = f'{self.PLATFORM_PREFIX}{normalized}' - self.platform_symbol_map.setdefault(symbol, machine) + symbol = self._platform_to_symbol_map.get(machine) + if symbol is None: + # Fallback calculation (should not happen if precomputed) + normalized = re.sub(r'[^A-Z0-9]', '_', machine.upper()) + symbol = f'{self.PLATFORM_PREFIX}{normalized}' + self._platform_to_symbol_map[machine] = symbol return symbol def _escape(self, value: str) -> str: diff --git a/src/oebuild/app/plugins/neo_generate/neo_generate.py b/src/oebuild/app/plugins/neo_generate/neo_generate.py index 571b7c0..101636d 100644 --- a/src/oebuild/app/plugins/neo_generate/neo_generate.py +++ b/src/oebuild/app/plugins/neo_generate/neo_generate.py @@ -17,7 +17,7 @@ import sys import textwrap from shutil import rmtree -from menuconfig_generator import NeoMenuconfigGenerator +from menuconfig_generator import MenuconfigSelection, NeoMenuconfigGenerator from prettytable import HRuleStyle, PrettyTable, TableStyle, VRuleStyle from ruamel.yaml.scalarstring import LiteralScalarString @@ -98,8 +98,7 @@ class NeoGenerate(OebuildCommand): 'Menuconfig was exited without applying any configuration; nothing generated.' ) return - parsed_args.platform = menu_selection.platform - parsed_args.features = menu_selection.features + self._apply_menu_selection(parsed_args, menu_selection) try: resolution = self._resolve_features( @@ -190,6 +189,24 @@ class NeoGenerate(OebuildCommand): logger.error('Menuconfig failed: %s', exc) sys.exit(-1) + def _apply_menu_selection( + self, parsed_args: argparse.Namespace, selection: MenuconfigSelection + ): + parsed_args.platform = selection.platform + parsed_args.features = selection.features + parsed_args.build_in = selection.build_in + parsed_args.no_fetch = selection.no_fetch + parsed_args.no_layer = selection.no_layer + parsed_args.sstate_mirrors = selection.sstate_mirrors + parsed_args.sstate_dir = selection.sstate_dir + parsed_args.tmp_dir = selection.tmp_dir + parsed_args.toolchain_dir = selection.toolchain_dir + parsed_args.llvm_toolchain_dir = selection.llvm_toolchain_dir + parsed_args.nativesdk_dir = selection.nativesdk_dir + parsed_args.datetime = selection.datetime + parsed_args.cache_src_dir = selection.cache_src_dir + parsed_args.directory = selection.directory + def _collect_params(self, args, build_dir): return { 'platform': args.platform, -- Gitee From 60a85da0663265ab5c6d96461c9aaad28bea8524 Mon Sep 17 00:00:00 2001 From: Egg12138 Date: Tue, 16 Dec 2025 20:12:26 +0800 Subject: [PATCH 3/3] neo-generate: add simple test for menuconfig - add some mocked feature file contexts assert each generated kconfig content Signed-off-by: egg12138 --- .../neo_generate/test_menuconfig_generator.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/oebuild/app/plugins/neo_generate/test_menuconfig_generator.py diff --git a/src/oebuild/app/plugins/neo_generate/test_menuconfig_generator.py b/src/oebuild/app/plugins/neo_generate/test_menuconfig_generator.py new file mode 100644 index 0000000..dfe50ed --- /dev/null +++ b/src/oebuild/app/plugins/neo_generate/test_menuconfig_generator.py @@ -0,0 +1,122 @@ +import os +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + +from oebuild.nightly_features import FeatureRegistry + +sys.path.insert(0, os.path.dirname(__file__)) +from menuconfig_generator import NeoMenuconfigGenerator # noqa: E402 + + +class MenuconfigGeneratorTest(unittest.TestCase): + """Smoke tests that validate the generated Kconfig structure.""" + + def setUp(self): + self.workspace = tempfile.TemporaryDirectory() + base = Path(self.workspace.name) + self.nightly_dir = base / 'nightly-features' + self.platform_dir = base / 'platform' + (self.nightly_dir / 'system').mkdir(parents=True) + (self.nightly_dir / 'containers').mkdir(parents=True) + self.platform_dir.mkdir(parents=True) + (self.platform_dir / 'qemu-aarch64.yaml').write_text('') + self._write_feature( + 'system', + 'system', + textwrap.dedent( + """\ + id: system + name: System Services + prompt: > + System services backbone + machines: + - qemu-aarch64 + sub_feats: + - id: ssh + name: SSH Access + - id: console + name: Console Access + one_of: + - self/ssh + - self/console + default_one_of: self/ssh + """ + ), + ) + self._write_feature( + 'containers', + 'containers', + textwrap.dedent( + """\ + id: containers + name: Container Platform + prompt: Container tooling stack + dependencies: + - system/system + sub_feats: + - id: containerd + name: containerd runtime + - id: isulad + name: Isulad runtime + choice: + - self/containerd + - self/isulad + """ + ), + ) + self._write_feature( + 'containers', + 'podman', + textwrap.dedent( + """\ + id: podman + name: Podman runtime + dependencies: + - containers + """ + ), + ) + + def tearDown(self): + self.workspace.cleanup() + + def _write_feature(self, category: str, file_name: str, content: str) -> None: + target = self.nightly_dir / category / f'{file_name}.yaml' + target.write_text(content) + + def _build_kconfig(self) -> str: + registry = FeatureRegistry(self.nightly_dir) + generator = NeoMenuconfigGenerator( + registry=registry, + platform_dir=self.platform_dir, + default_platform='qemu-aarch64', + ) + return generator.build_kconfig_text() + + def test_categories_and_feature_blocks_are_emitted(self): + kconfig = self._build_kconfig() + self.assertIn('menu "System"', kconfig) + self.assertIn('config FEATURE_SYSTEM', kconfig) + self.assertIn('menu "Containers"', kconfig) + + def test_one_of_and_choice_layout_and_dependencies(self): + kconfig = self._build_kconfig() + self.assertIn('prompt "Select mode for System Services"', kconfig) + self.assertIn('default FEATURE_SYSTEM_SSH', kconfig) + self.assertIn('menu "Optional Container Platform add-ons"', kconfig) + self.assertIn('depends on PLATFORM_QEMU_AARCH64', kconfig) + self.assertIn('select FEATURE_SYSTEM', kconfig) + self.assertIn('if FEATURE_CONTAINERS', kconfig) + self.assertIn('if FEATURE_SYSTEM', kconfig) + self.assertIn('config FEATURE_CONTAINERS_PODMAN', kconfig) + if_block_start = kconfig.index(' if FEATURE_CONTAINERS') + podman_pos = kconfig.index(' config FEATURE_CONTAINERS_PODMAN') + endif_pos = kconfig.index(' endif', podman_pos) + self.assertTrue(if_block_start < podman_pos < endif_pos) + + +if __name__ == '__main__': + unittest.main() -- Gitee