diff --git a/src/oebuild/app/conf/plugins.yaml b/src/oebuild/app/conf/plugins.yaml index 4fa8b1e5a2e790d217c042c957d6f3165520e2ec..2c62fd974dc9ff862a0042164099cafa672ea086 100644 --- a/src/oebuild/app/conf/plugins.yaml +++ b/src/oebuild/app/conf/plugins.yaml @@ -14,3 +14,6 @@ plugins: - name: manifest class: Manifest path: plugins/manifest/manifest.py +- name: clear + class: Clear + path: plugins/clear/clear.py diff --git a/src/oebuild/app/main.py b/src/oebuild/app/main.py index aa62d6f016d74b0795669942e938dc85dd3947ae..60c4c0d0ceaba7b74193a8140bd98429cda74a54 100644 --- a/src/oebuild/app/main.py +++ b/src/oebuild/app/main.py @@ -23,8 +23,10 @@ import colorama import oebuild.util as oebuild_util from oebuild.version import __version__ -from oebuild.command import ExtCommand, extension_commands +from oebuild.spec import get_spec,_ExtCommand +from oebuild.command import OebuildCommand from oebuild.m_log import logger +from oebuild.oebuild_parser import OebuildArgumentParser,OebuildHelpAction APP = "app" @@ -36,7 +38,6 @@ class OebuildApp: def __init__(self): self.base_oebuild_dir = oebuild_util.get_base_oebuild() self.oebuild_parser = None - self.subparser_gen = None self.subparsers = {} self.cmd = None try: @@ -53,7 +54,7 @@ class OebuildApp: ''' command_ext = OrderedDict() for app in plugins: - command_ext[app['name']] = ExtCommand( + command_ext[app['name']] = _ExtCommand( name=app['name'], class_name=app['class'], path=app['path']) @@ -71,12 +72,10 @@ class OebuildApp: # Add sub-parsers for the command_ext commands. for command_name in self.command_ext: - subparser = subparser_gen.add_parser(command_name, add_help=False) - self.subparsers[command_name] = subparser + self.subparsers[command_name] = subparser_gen.add_parser(command_name, add_help=False) # Save the instance state. self.oebuild_parser = oebuild_parser - self.subparser_gen = subparser_gen def make_parsers(self,): ''' @@ -130,15 +129,16 @@ class OebuildApp: # Finally, run the command. self._run_extension(args.command, unknown) - def _run_extension(self, name, unknown): + def _run_extension(self, name:str, unknown): # Check a program invariant. - self.cmd =self.command_spec[name].factory() + spec = self.command_spec[name] + cmd:OebuildCommand = oebuild_util.get_instance(spec.factory) parser = self.subparsers[name] - args = self.cmd.add_parser(parser) - self.cmd.run(args, unknown) + args = cmd.add_parser(parser_adder=parser) + cmd.run(args, unknown) def run(self, argv): ''' @@ -156,184 +156,31 @@ class OebuildApp: ''' print help message ''' - self.oebuild_parser.print_help(top_level=True) - - -class OebuildHelpAction(argparse.Action): - ''' - set argparse help is true - ''' - def __call__(self, parser, namespace, values, option_string=None): - # Just mark that help was requested. - namespace.help = True - - -class OebuildArgumentParser(argparse.ArgumentParser): - ''' - The argparse module is infuriatingly coy about its parser and - help formatting APIs, marking almost everything you need to - customize help output an "implementation detail". Even accessing - the parser's description and epilog attributes as we do here is - technically breaking the rules. - - Even though the implementation details have been pretty stable - since the module was first introduced in Python 3.2, let's avoid - possible headaches by overriding some "proper" argparse APIs - here instead of monkey-patching the module or breaking - abstraction barriers. This is duplicative but more future-proof. - ''' - - def __init__(self, *args, **kwargs): - # The super constructor calls add_argument(), so this has to - # come first as our override of that method relies on it. - self.oebuild_optionals = [] - self.oebuild_app = kwargs.pop('oebuild_app', None) - super(OebuildArgumentParser, self).__init__(*args, **kwargs) - - def print_help(self, file=None, top_level=False): - print(self.format_help(top_level=top_level), end='', - file=file or sys.stdout) - - def format_help(self, top_level=False): - # When top_level is True, we override the parent method to - # produce more readable output, which separates commands into - # logical groups. In order to print optionals, we rely on the - # data available in our add_argument() override below. - # - # If top_level is False, it's because we're being called from - # one of the subcommand parsers, and we delegate to super. - - if not top_level: - return super(OebuildArgumentParser, self).format_help() - - # Format the help to be at most 75 columns wide, the maximum - # generally recommended by typographers for readability. - # - # If the terminal width (COLUMNS) is less than 75, use width - # (COLUMNS - 2) instead, unless that is less than 30 columns - # wide, which we treat as a hard minimum. - width = min(75, max(shutil.get_terminal_size().columns - 2, 30)) - - with StringIO() as sio: - - def append(*strings): - for s_t in strings: - print(s_t, file=sio) - - append(self.format_usage(), - self.description, - '') - - append('optional arguments:') - for w_o in self.oebuild_optionals: - self._format_oebuild_optional(append, w_o, width) - - append('') - for _,command in self.oebuild_app.command_spec.items(): - self._format_command(append, command, width) - - if self.epilog: - append(self.epilog) - - return sio.getvalue() - - def _format_oebuild_optional(self, append, w_o, width): - metavar = w_o['metavar'] - options = w_o['options'] - help_msg = w_o.get('help') - - # Join the various options together as a comma-separated list, - # with the metavar if there is one. That's our "thing". - if metavar is not None: - opt_str = ' ' + ', '.join(f'{o} {metavar}' for o in options) - else: - opt_str = ' ' + ', '.join(options) - - # Delegate to the generic formatter. - self._format_thing_and_help(append, opt_str, help_msg, width) - - def _format_command(self, append, command, width): - thing = f' {command.name}:' - self._format_thing_and_help(append, thing, command.help, width) - - def _format_extension_spec(self, append, spec, width): - self._format_thing_and_help(append, ' ' + spec.name + ':', - spec.help, width) - - def _format_thing_and_help(self, append, thing, help_msg, width): - # Format help for some "thing" (arbitrary text) and its - # corresponding help text an argparse-like way. - help_offset = min(max(10, width - 20), 24) - help_indent = ' ' * help_offset - - thinglen = len(thing) - - if help_msg is None: - # If there's no help string, just print the thing. - append(thing) - else: - # Reflow the lines in help to the desired with, using - # the help_offset as an initial indent. - help_msg = ' '.join(help_msg.split()) - help_lines = textwrap.wrap(help_msg, width=width, - initial_indent=help_indent, - subsequent_indent=help_indent) - - if thinglen > help_offset - 1: - # If the "thing" (plus room for a space) is longer - # than the initial help offset, print it on its own - # line, followed by the help on subsequent lines. - append(thing) - append(*help_lines) - else: - # The "thing" is short enough that we can start - # printing help on the same line without overflowing - # the help offset, so combine the "thing" with the - # first line of help. - help_lines[0] = thing + help_lines[0][thinglen:] - append(*help_lines) - - def add_argument(self, *args, **kwargs): - # Track information we want for formatting help. The argparse - # module calls kwargs.pop(), so can't call super first without - # losing data. - optional = {'options': [], 'metavar': kwargs.get('metavar', None)} - need_metavar = (optional['metavar'] is None and - kwargs.get('action') in (None, 'store')) - for arg in args: - if not arg.startswith('-'): - break - optional['options'].append(arg) - # If no metavar was given, the last option name is - # used. By convention, long options go last, so this - # matches the default argparse behavior. - if need_metavar: - optional['metavar'] = arg.lstrip('-').translate( - {ord('-'): '_'}).upper() - optional['help'] = kwargs.get('help') - self.oebuild_optionals.append(optional) - - # Let argparse handle the actual argument. - super().add_argument(*args, **kwargs) - - def error(self, message): - # if (self.oebuild_app and - # self.oebuild_app.mle and - # isinstance(self.oebuild_app.mle, - # ManifestVersionError) and - # self.oebuild_app.cmd): - # self.oebuild_app.cmd.die(mve_msg(self.west_app.mle)) - super().error(message=message) + self.oebuild_parser.print_help() def check_user(): ''' check execute user must in normal user ''' if getpass.getuser() == "root": - log.err("can not use oebuild in root") + logger.error("can not use oebuild in root") return False return True +def extension_commands(pre_dir, commandlist:OrderedDict): + ''' + Get descriptions of available extension commands. + The return value is an ordered map from project paths to lists of + OebuildExtCommandSpec objects, for projects which define extension + commands. The map's iteration order matches the manifest.projects + order. + ''' + specs = OrderedDict() + for key, value in commandlist.items(): + specs[key] = get_spec(pre_dir, value) + + return specs + def main(argv=None): ''' oebuild main entrypoint diff --git a/src/oebuild/app/plugins/clear/clear.py b/src/oebuild/app/plugins/clear/clear.py new file mode 100644 index 0000000000000000000000000000000000000000..80bc4138884d0f11ed0659ead2cfc9e3948f477c --- /dev/null +++ b/src/oebuild/app/plugins/clear/clear.py @@ -0,0 +1,97 @@ +''' +Copyright (c) 2023 openEuler Embedded +oebuild is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. +''' + +import argparse +import textwrap +import os +import pathlib + +from oebuild.command import OebuildCommand +import oebuild.util as oebuild_util +from oebuild.configure import Configure +from oebuild.docker_proxy import DockerProxy +from oebuild.m_log import logger + +class Clear(OebuildCommand): + + def __init__(self): + self.configure = Configure() + self.client = DockerProxy() + super().__init__( + 'clear', + 'clear someone which oebuild generate', + textwrap.dedent('''\ + During the construction process using oebuild, a lot of temporary products + will be generated, such as containers,so this command can remove unimportant + products, such as containers +''' + )) + + def do_add_parser(self, parser_adder) -> argparse.ArgumentParser: + parser = self._parser( + parser_adder, + usage=''' + + %(prog)s [docker] +''') + + parser.add_argument( + 'item', nargs='?', default=None, + help='''The name of the directory that will be initialized''') + + return parser + + def do_run(self, args: argparse.Namespace, unknown = None): + args = args.parse_args(unknown) + + if args.item == "docker": + self.clear_docker() + + def clear_docker(self,): + ''' + clear container + ''' + # get all build directory and get .env from every build directory + logger.info("clearing container, please waiting ...") + env_list = [] + build_list = os.listdir(self.configure.build_dir()) + for build_dir in build_list: + build_dir = os.path.join(self.configure.build_dir(), build_dir) + if os.path.exists(os.path.join(build_dir,".env")): + env_list.append(os.path.join(build_dir,".env")) + + # traversal every env file and get container_id, and then try to stop it and rm it + for env in env_list: + env_conf = oebuild_util.read_yaml(pathlib.Path(env)) + try: + container_id = env_conf['container']['short_id'] + container = self.client.get_container(container_id=container_id) + DockerProxy().stop_container(container=container) + DockerProxy().delete_container(container=container) + logger.info(f"delete container: {container.short_id} successful") + except: + continue + + # get all container which name start with oebuild and delete it, + # in case when user rm build directory then legacy container + # containers = self.client.get_all_container() + # for container in containers: + # container_name = str(container.attrs.get('Name')).lstrip("/") + # if container_name.startswith("oebuild_"): + # try: + # DockerProxy().stop_container(container=container) + # DockerProxy().delete_container(container=container) + # logger.info(f"delete container: {container.short_id} successful") + # except: + # continue + + logger.info("clear container finished") diff --git a/src/oebuild/app/plugins/init/init.py b/src/oebuild/app/plugins/init/init.py index 87ede08a9ff952d85f65eda8494c702f753048ea..9a68819bbce53b539fc628b13bfd3eca6a00d197 100644 --- a/src/oebuild/app/plugins/init/init.py +++ b/src/oebuild/app/plugins/init/init.py @@ -43,26 +43,26 @@ class Init(OebuildCommand): )) def do_add_parser(self, parser_adder): - parser = self._parser( + self._parser( parser_adder, usage=''' %(prog)s [directory] [-u yocto_remote_url] [-b branch] ''') - parser.add_argument('-u', dest = 'yocto_remote_url', + parser_adder.add_argument('-u', dest = 'yocto_remote_url', help='''Specifies the remote of yocto-meta-openeuler''') - parser.add_argument('-b', dest = 'branch', + parser_adder.add_argument('-b', dest = 'branch', help='''Specifies the branch of yocto-meta-openeuler''') - parser.add_argument( + parser_adder.add_argument( 'directory', nargs='?', default=None, help='''The name of the directory that will be initialized''') - return parser + return parser_adder - def do_run(self, args: argparse.Namespace, unknown = None): + def do_run(self, args: argparse.ArgumentParser, unknown = None): ''' detach target dicrectory if finished init, if inited, just put out err msg and exit ''' diff --git a/src/oebuild/command.py b/src/oebuild/command.py index 20d1a1fe5c60a543771188d2f7744920280655d0..8af8072d26180cc0af62de7bcef08959606376b4 100644 --- a/src/oebuild/command.py +++ b/src/oebuild/command.py @@ -12,11 +12,6 @@ See the Mulan PSL v2 for more details. from abc import ABC, abstractmethod import argparse -import importlib.util -from collections import OrderedDict -import os -from dataclasses import dataclass -import sys from typing import List import colorama @@ -33,32 +28,38 @@ class OebuildCommand(ABC): self.name = name self.help_msg = help_msg self.description = description - self.parser = None + self.parser:argparse.ArgumentParser = None - def run(self, args: argparse.Namespace, unknown: List[str]): + def run(self, args: argparse.ArgumentParser, unknown: List[str]): ''' The executing body, each inherited class will register the executor with the executor body for execution ''' + pars = args.parse_args(unknown) + if pars.help: + self.print_help_msg() + return self.do_run(args=args, unknown=unknown) - def add_parser(self, parser_adder): + def add_parser(self, parser_adder: argparse.ArgumentParser): ''' Registers a parser for this command, and returns it. The parser object is stored in a ``parser`` attribute. :param parser_adder: The return value of a call to ``argparse.ArgumentParser.add_subparsers()`` ''' - parser = self.do_add_parser(parser_adder) + self.parser:argparse.ArgumentParser = self.do_add_parser(parser_adder) - if parser is None: + if self.parser is None: raise ValueError('do_add_parser did not return a value') - self.parser = parser + self.parser.add_argument('-h', '--help', dest="help", action="store_true", + help='get help for oebuild or a command') + return self.parser @abstractmethod - def do_add_parser(self, parser_adder): + def do_add_parser(self, parser_adder: argparse.ArgumentParser): ''' The directive registers the interface, which the successor needs to implement ''' @@ -72,13 +73,13 @@ class OebuildCommand(ABC): sequence of un-parsed argument strings. ''' - def print_help(self, args: argparse.Namespace): + def print_help_msg(self): ''' print help message ''' - args = args.parse_args(['-h']) + self.parser.print_help() - def _parser(self, parser, **kwargs): + def _parser(self, parser: argparse.ArgumentParser, **kwargs): # return a "standard" parser. kwargs['help'] = self.help_msg @@ -88,136 +89,3 @@ class OebuildCommand(ABC): parser.__dict__.update(kwargs) return parser - - -class CommandError(RuntimeError): - ''' - Indicates that a command failed. - ''' - def __init__(self, returncode=1): - super().__init__() - self.returncode = returncode - - -class CommandContextError(CommandError): - '''Indicates that a context-dependent command could not be run.''' - - -class ExtensionCommandError(CommandError): - '''Exception class indicating an extension command was badly - defined and could not be created.''' - - - def __init__(self, **kwargs): - self.hint = kwargs.pop('hint', None) - super(ExtensionCommandError, self).__init__(**kwargs) - -@dataclass -class _CmdFactory: - - py_file: str - name: str - attr: str - - def __call__(self): - # Append the python file's directory to sys.path. This lets - # its code import helper modules in a natural way. - py_dir = os.path.dirname(self.py_file) - sys.path.append(py_dir) - - # Load the module containing the command. Convert only - # expected exceptions to ExtensionCommandError. - try: - mod = _commands_module_from_file(self.py_file, self.attr) - except ImportError as i_e: - raise ExtensionCommandError( - hint=f'could not import {self.py_file}') from i_e - - # Get the attribute which provides the OebuildCommand subclass. - try: - cls = getattr(mod, self.attr) - except AttributeError as a_e: - raise ExtensionCommandError( - hint=f'no attribute {self.attr} in {self.py_file}') from a_e - - # Create the command instance and return it. - try: - return cls() - except Exception as e_p: - raise ExtensionCommandError( - hint='command constructor threw an exception') from e_p - - -@dataclass -class OebuildExtCommandSpec: - ''' - An object which allows instantiating a oebuild extension. - ''' - - # Command name, as known to the user - name: str - - description: str - - help: str - - # This returns a OebuildCommand instance when called. - # It may do some additional steps (like importing the definition of - # the command) before constructing it, however. - factory: _CmdFactory - - -@dataclass -class ExtCommand: - ''' - record extern command basic info, it's useful when app initialize - ''' - name: str - - class_name: str - - path: str - - -def extension_commands(pre_dir, commandlist:OrderedDict): - ''' - Get descriptions of available extension commands. - The return value is an ordered map from project paths to lists of - OebuildExtCommandSpec objects, for projects which define extension - commands. The map's iteration order matches the manifest.projects - order. - ''' - specs = OrderedDict() - for key, value in commandlist.items(): - specs[key] = _ext_specs(pre_dir, value) - - return specs - - -def _ext_specs(pre_dir, command_ext: ExtCommand): - - py_file = os.path.join(os.path.dirname(__file__), pre_dir, command_ext.path) - - factory = _CmdFactory(py_file=py_file, name=command_ext.name, attr=command_ext.class_name) - - return OebuildExtCommandSpec( - name=command_ext.name, - description=factory().description, - help=factory().help_msg, - factory=factory) - - -def _commands_module_from_file(file, mod_name): - ''' - Python magic for importing a module containing oebuild extension - commands. To avoid polluting the sys.modules key space, we put - these modules in an (otherwise unpopulated) oebuild.commands.ext - package. - ''' - spec = importlib.util.spec_from_file_location(mod_name, file) - if spec is None: - return None - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - - return mod diff --git a/src/oebuild/docker_proxy.py b/src/oebuild/docker_proxy.py index 9e7629002fe26f7dae87a11c11f26fcc5c3db978..dceabc577e0d47b11177672f00abc0687e660125 100644 --- a/src/oebuild/docker_proxy.py +++ b/src/oebuild/docker_proxy.py @@ -125,6 +125,15 @@ class DockerProxy: ''' return self._docker.containers.get(container_id=container_id) + + def get_all_container(self): + ''' + get all container like command 'docker ps -a' + args: + None + ''' + return self._docker.containers.list(all=True) + @staticmethod def stop_container(container: Container): ''' @@ -268,6 +277,8 @@ class DockerProxy: detach=True, tty=True ) + container_name = str(container.attrs.get('Name')).lstrip("/") + container.rename(f"oebuild_{container_name}") return container def container_exec_with_tty(self, diff --git a/src/oebuild/oebuild_parser.py b/src/oebuild/oebuild_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..055a5ff653dbe69a5ce158b077733bdbca512823 --- /dev/null +++ b/src/oebuild/oebuild_parser.py @@ -0,0 +1,180 @@ +''' +Copyright (c) 2023 openEuler Embedded +oebuild is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. +''' + +import argparse +import sys +import shutil +from io import StringIO +import textwrap + +class OebuildHelpAction(argparse.Action): + ''' + set argparse help is true + ''' + def __call__(self, parser, namespace, values, option_string=None): + # Just mark that help was requested. + namespace.help = True + + +class OebuildArgumentParser(argparse.ArgumentParser): + ''' + The argparse module is infuriatingly coy about its parser and + help formatting APIs, marking almost everything you need to + customize help output an "implementation detail". Even accessing + the parser's description and epilog attributes as we do here is + technically breaking the rules. + + Even though the implementation details have been pretty stable + since the module was first introduced in Python 3.2, let's avoid + possible headaches by overriding some "proper" argparse APIs + here instead of monkey-patching the module or breaking + abstraction barriers. This is duplicative but more future-proof. + ''' + + def __init__(self, *args, **kwargs): + # The super constructor calls add_argument(), so this has to + # come first as our override of that method relies on it. + self.oebuild_optionals = [] + self.oebuild_app = kwargs.pop('oebuild_app', None) + super(OebuildArgumentParser, self).__init__(*args, **kwargs) + + def print_help(self, file=None): + print(self.format_help(), end='', + file=file or sys.stdout) + + def format_help(self): + # When top_level is True, we override the parent method to + # produce more readable output, which separates commands into + # logical groups. In order to print optionals, we rely on the + # data available in our add_argument() override below. + # + # If top_level is False, it's because we're being called from + # one of the subcommand parsers, and we delegate to super. + + # if not top_level: + # return super(OebuildArgumentParser, self).format_help() + + # Format the help to be at most 75 columns wide, the maximum + # generally recommended by typographers for readability. + # + # If the terminal width (COLUMNS) is less than 75, use width + # (COLUMNS - 2) instead, unless that is less than 30 columns + # wide, which we treat as a hard minimum. + width = min(100, max(shutil.get_terminal_size().columns - 2, 30)) + + with StringIO() as sio: + + def append(*strings): + for s_t in strings: + print(s_t, file=sio) + + append(self.format_usage(), + self.description, + '') + + append('optional arguments:') + for w_o in self.oebuild_optionals: + self._format_oebuild_optional(append, w_o, width) + + if self.oebuild_app is not None: + append('') + for _,command in self.oebuild_app.command_spec.items(): + self._format_command(append, command, width) + + if self.epilog: + append(self.epilog) + + return sio.getvalue() + + def _format_oebuild_optional(self, append, w_o, width): + metavar = w_o['metavar'] + options = w_o['options'] + help_msg = w_o.get('help', "") + + # Join the various options together as a comma-separated list, + # with the metavar if there is one. That's our "thing". + if metavar is not None: + opt_str = ' ' + ', '.join(f'{o} {metavar}' for o in options) + else: + opt_str = ' ' + ', '.join(options) + + # Delegate to the generic formatter. + self._format_thing_and_help(append, opt_str, help_msg, width) + + def _format_command(self, append, command, width): + thing = f' {command.name}:' + self._format_thing_and_help(append, thing, command.help, width) + + def _format_thing_and_help(self, append, thing, help_msg: str, width): + # Format help for some "thing" (arbitrary text) and its + # corresponding help text an argparse-like way. + help_offset = min(max(10, width - 20), 20) + help_indent = ' ' * help_offset + + thinglen = len(thing) + + if help_msg is None: + # If there's no help string, just print the thing. + append(thing) + else: + # Reflow the lines in help to the desired with, using + # the help_offset as an initial indent. + help_msg = ' '.join(help_msg.split()) + help_lines = textwrap.wrap(help_msg, width=width, + initial_indent=help_indent, + subsequent_indent=help_indent) + + if thinglen > help_offset - 1: + # If the "thing" (plus room for a space) is longer + # than the initial help offset, print it on its own + # line, followed by the help on subsequent lines. + append(thing) + append(*help_lines) + else: + # The "thing" is short enough that we can start + # printing help on the same line without overflowing + # the help offset, so combine the "thing" with the + # first line of help. + help_lines[0] = thing + help_lines[0][thinglen:] + append(*help_lines) + + def add_argument(self, *args, **kwargs): + # Track information we want for formatting help. The argparse + # module calls kwargs.pop(), so can't call super first without + # losing data. + optional = {'options': [], 'metavar': kwargs.get('metavar', None)} + need_metavar = (optional['metavar'] is None and + kwargs.get('action') in (None, 'store')) + for arg in args: + if not arg.startswith('-'): + break + optional['options'].append(arg) + # If no metavar was given, the last option name is + # used. By convention, long options go last, so this + # matches the default argparse behavior. + if need_metavar: + optional['metavar'] = arg.lstrip('-').translate( + {ord('-'): '_'}).upper() + optional['help'] = kwargs.get('help') + self.oebuild_optionals.append(optional) + + # Let argparse handle the actual argument. + super().add_argument(*args, **kwargs) + + def error(self, message): + # if (self.oebuild_app and + # self.oebuild_app.mle and + # isinstance(self.oebuild_app.mle, + # ManifestVersionError) and + # self.oebuild_app.cmd): + # self.oebuild_app.cmd.die(mve_msg(self.west_app.mle)) + super().error(message=message) diff --git a/src/oebuild/spec.py b/src/oebuild/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..84c8520771d2545796560c6f11479fd955cc9b39 --- /dev/null +++ b/src/oebuild/spec.py @@ -0,0 +1,133 @@ +''' +Copyright (c) 2023 openEuler Embedded +oebuild is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. +''' + +import importlib.util +import os +from dataclasses import dataclass +import sys + +class CommandError(RuntimeError): + ''' + Indicates that a command failed. + ''' + def __init__(self, returncode=1): + super().__init__() + self.returncode = returncode + + +class CommandContextError(CommandError): + '''Indicates that a context-dependent command could not be run.''' + + +class ExtensionCommandError(CommandError): + '''Exception class indicating an extension command was badly + defined and could not be created.''' + + + def __init__(self, **kwargs): + self.hint = kwargs.pop('hint', None) + super(ExtensionCommandError, self).__init__(**kwargs) + +@dataclass +class _CmdFactory: + + py_file: str + name: str + attr: str + + def __call__(self): + # Append the python file's directory to sys.path. This lets + # its code import helper modules in a natural way. + py_dir = os.path.dirname(self.py_file) + sys.path.append(py_dir) + + # Load the module containing the command. Convert only + # expected exceptions to ExtensionCommandError. + try: + mod = _commands_module_from_file(self.py_file, self.attr) + except ImportError as i_e: + raise ExtensionCommandError( + hint=f'could not import {self.py_file}') from i_e + + # Get the attribute which provides the OebuildCommand subclass. + try: + cls = getattr(mod, self.attr) + except AttributeError as a_e: + raise ExtensionCommandError( + hint=f'no attribute {self.attr} in {self.py_file}') from a_e + + # Create the command instance and return it. + try: + return cls() + except Exception as e_p: + raise ExtensionCommandError( + hint='command constructor threw an exception') from e_p + + +@dataclass +class OebuildExtCommandSpec: + ''' + An object which allows instantiating a oebuild extension. + ''' + + # Command name, as known to the user + name: str + + description: str + + help: str + + # This returns a OebuildCommand instance when called. + # It may do some additional steps (like importing the definition of + # the command) before constructing it, however. + factory: _CmdFactory + +@dataclass +class _ExtCommand: + ''' + record extern command basic info, it's useful when app initialize + ''' + name: str + + class_name: str + + path: str + + +def get_spec(pre_dir, command_ext: _ExtCommand): + ''' + xxx + ''' + + py_file = os.path.join(os.path.dirname(__file__), pre_dir, command_ext.path) + + factory = _CmdFactory(py_file=py_file, name=command_ext.name, attr=command_ext.class_name) + + return OebuildExtCommandSpec( + name=command_ext.name, + description=factory().description, + help=factory().help_msg, + factory=factory) + + +def _commands_module_from_file(file, mod_name): + ''' + Python magic for importing a module containing oebuild extension + commands. To avoid polluting the sys.modules key space, we put + these modules in an (otherwise unpopulated) oebuild.commands.ext + package. + ''' + spec = importlib.util.spec_from_file_location(mod_name, file) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + return mod diff --git a/src/oebuild/util.py b/src/oebuild/util.py index 80f91c01b0fd387642545bc426b2f4c784df5c82..d012103f38158612b90897e8a5a5d97132b67ded 100644 --- a/src/oebuild/util.py +++ b/src/oebuild/util.py @@ -125,3 +125,9 @@ please install docker first, and run follow commands in root: 3, systemctl daemon-reload && systemctl restart docker 4, chmod o+rw /var/run/docker.sock ''') from exc + +def get_instance(factory): + ''' + Instantiate a class + ''' + return factory()