1import importlib 2import os 3import pkgutil 4import signal 5import sys 6from collections import defaultdict 7from difflib import get_close_matches 8from inspect import getmembers 9 10from colorama import Style 11 12from conans import __version__ as client_version 13from conans.cli.command import ConanSubCommand 14from conans.cli.exit_codes import SUCCESS, ERROR_MIGRATION, ERROR_GENERAL, USER_CTRL_C, \ 15 ERROR_SIGTERM, USER_CTRL_BREAK, ERROR_INVALID_CONFIGURATION 16from conans.client.api.conan_api import Conan 17from conans.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError 18from conans.util.files import exception_message_safe 19from conans.util.log import logger 20 21 22class Cli(object): 23 """A single command of the conan application, with all the first level commands. Manages the 24 parsing of parameters and delegates functionality to the conan python api. It can also show the 25 help of the tool. 26 """ 27 28 def __init__(self, conan_api): 29 assert isinstance(conan_api, Conan), "Expected 'Conan' type, got '{}'".format( 30 type(conan_api)) 31 self._conan_api = conan_api 32 self._out = conan_api.out 33 self._groups = defaultdict(list) 34 self._commands = {} 35 conan_commands_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "commands") 36 for module in pkgutil.iter_modules([conan_commands_path]): 37 module_name = module[1] 38 self._add_command("conans.cli.commands.{}".format(module_name), module_name) 39 user_commands_path = os.path.join(self._conan_api.cache_folder, "commands") 40 sys.path.append(user_commands_path) 41 for module in pkgutil.iter_modules([user_commands_path]): 42 module_name = module[1] 43 if module_name.startswith("cmd_"): 44 self._add_command(module_name, module_name.replace("cmd_", "")) 45 46 def _add_command(self, import_path, method_name): 47 try: 48 command_wrapper = getattr(importlib.import_module(import_path), method_name) 49 if command_wrapper.doc: 50 self._commands[command_wrapper.name] = command_wrapper 51 self._groups[command_wrapper.group].append(command_wrapper.name) 52 for name, value in getmembers(importlib.import_module(import_path)): 53 if isinstance(value, ConanSubCommand): 54 if name.startswith("{}_".format(method_name)): 55 command_wrapper.add_subcommand(value) 56 else: 57 raise ConanException("The name for the subcommand method should " 58 "begin with the main command name + '_'. " 59 "i.e. {}_<subcommand_name>".format(method_name)) 60 except AttributeError: 61 raise ConanException("There is no {} method defined in {}".format(method_name, 62 import_path)) 63 64 @property 65 def conan_api(self): 66 return self._conan_api 67 68 @property 69 def commands(self): 70 return self._commands 71 72 @property 73 def groups(self): 74 return self._groups 75 76 def _print_similar(self, command): 77 """ Looks for similar commands and prints them if found. 78 """ 79 matches = get_close_matches( 80 word=command, possibilities=self.commands.keys(), n=5, cutoff=0.75) 81 82 if len(matches) == 0: 83 return 84 85 if len(matches) > 1: 86 self._out.info("The most similar commands are") 87 else: 88 self._out.info("The most similar command is") 89 90 for match in matches: 91 self._out.info(" %s" % match) 92 93 self._out.info("") 94 95 def help_message(self): 96 self.commands["help"].method(self.conan_api, self.commands["help"].parser, 97 commands=self.commands, groups=self.groups) 98 99 def run(self, *args): 100 """ Entry point for executing commands, dispatcher to class 101 methods 102 """ 103 version = sys.version_info 104 if version.major == 2 or version.minor <= 4: 105 raise ConanException( 106 "Unsupported Python version. Minimum required version is Python 3.5") 107 108 try: 109 command_argument = args[0][0] 110 except IndexError: # No parameters 111 self.help_message() 112 return SUCCESS 113 try: 114 command = self.commands[command_argument] 115 except KeyError as exc: 116 if command_argument in ["-v", "--version"]: 117 self._out.info("Conan version %s" % client_version) 118 return SUCCESS 119 120 if command_argument in ["-h", "--help"]: 121 self.help_message() 122 return SUCCESS 123 124 self._out.info("'%s' is not a Conan command. See 'conan --help'." % command_argument) 125 self._out.info("") 126 self._print_similar(command_argument) 127 raise ConanException("Unknown command %s" % str(exc)) 128 129 command.run(self.conan_api, self.commands[command_argument].parser, 130 args[0][1:], commands=self.commands, groups=self.groups) 131 132 return SUCCESS 133 134 135def cli_out_write(data, fg=None, bg=None): 136 data = "{}{}{}{}\n".format(fg or '', bg or '', data, Style.RESET_ALL) 137 sys.stdout.write(data) 138 139 140def main(args): 141 """ main entry point of the conan application, using a Command to 142 parse parameters 143 144 Exit codes for conan command: 145 146 0: Success (done) 147 1: General ConanException error (done) 148 2: Migration error 149 3: Ctrl+C 150 4: Ctrl+Break 151 5: SIGTERM 152 6: Invalid configuration (done) 153 """ 154 try: 155 conan_api = Conan(quiet=False) 156 except ConanMigrationError: # Error migrating 157 sys.exit(ERROR_MIGRATION) 158 except ConanException as e: 159 sys.stderr.write("Error in Conan initialization: {}".format(e)) 160 sys.exit(ERROR_GENERAL) 161 162 def ctrl_c_handler(_, __): 163 print('You pressed Ctrl+C!') 164 sys.exit(USER_CTRL_C) 165 166 def sigterm_handler(_, __): 167 print('Received SIGTERM!') 168 sys.exit(ERROR_SIGTERM) 169 170 def ctrl_break_handler(_, __): 171 print('You pressed Ctrl+Break!') 172 sys.exit(USER_CTRL_BREAK) 173 174 signal.signal(signal.SIGINT, ctrl_c_handler) 175 signal.signal(signal.SIGTERM, sigterm_handler) 176 177 if sys.platform == 'win32': 178 signal.signal(signal.SIGBREAK, ctrl_break_handler) 179 180 try: 181 cli = Cli(conan_api) 182 exit_error = cli.run(args) 183 except SystemExit as exc: 184 if exc.code != 0: 185 logger.error(exc) 186 conan_api.out.error("Exiting with code: %d" % exc.code) 187 exit_error = exc.code 188 except ConanInvalidConfiguration as exc: 189 exit_error = ERROR_INVALID_CONFIGURATION 190 conan_api.out.error(exc) 191 except ConanException as exc: 192 exit_error = ERROR_GENERAL 193 conan_api.out.error(exc) 194 except Exception as exc: 195 import traceback 196 print(traceback.format_exc()) 197 exit_error = ERROR_GENERAL 198 msg = exception_message_safe(exc) 199 conan_api.out.error(msg) 200 201 sys.exit(exit_error) 202