summaryrefslogtreecommitdiff
path: root/buildstream/_frontend/app.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildstream/_frontend/app.py')
-rw-r--r--buildstream/_frontend/app.py870
1 files changed, 0 insertions, 870 deletions
diff --git a/buildstream/_frontend/app.py b/buildstream/_frontend/app.py
deleted file mode 100644
index d4ea83871..000000000
--- a/buildstream/_frontend/app.py
+++ /dev/null
@@ -1,870 +0,0 @@
-#
-# Copyright (C) 2016-2018 Codethink Limited
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-#
-# Authors:
-# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
-
-from contextlib import contextmanager
-import os
-import sys
-import traceback
-import datetime
-from textwrap import TextWrapper
-import click
-from click import UsageError
-
-# Import buildstream public symbols
-from .. import Scope
-
-# Import various buildstream internals
-from .._context import Context
-from .._platform import Platform
-from .._project import Project
-from .._exceptions import BstError, StreamError, LoadError, LoadErrorReason, AppError
-from .._message import Message, MessageType, unconditional_messages
-from .._stream import Stream
-from .._versions import BST_FORMAT_VERSION
-from .. import _yaml
-from .._scheduler import ElementJob, JobStatus
-
-# Import frontend assets
-from .profile import Profile
-from .status import Status
-from .widget import LogLine
-
-# Intendation for all logging
-INDENT = 4
-
-
-# App()
-#
-# Main Application State
-#
-# Args:
-# main_options (dict): The main CLI options of the `bst`
-# command, before any subcommand
-#
-class App():
-
- def __init__(self, main_options):
-
- #
- # Public members
- #
- self.context = None # The Context object
- self.stream = None # The Stream object
- self.project = None # The toplevel Project object
- self.logger = None # The LogLine object
- self.interactive = None # Whether we are running in interactive mode
- self.colors = None # Whether to use colors in logging
-
- #
- # Private members
- #
- self._session_start = datetime.datetime.now()
- self._session_name = None
- self._main_options = main_options # Main CLI options, before any command
- self._status = None # The Status object
- self._fail_messages = {} # Failure messages by unique plugin id
- self._interactive_failures = None # Whether to handle failures interactively
- self._started = False # Whether a session has started
-
- # UI Colors Profiles
- self._content_profile = Profile(fg='yellow')
- self._format_profile = Profile(fg='cyan', dim=True)
- self._success_profile = Profile(fg='green')
- self._error_profile = Profile(fg='red', dim=True)
- self._detail_profile = Profile(dim=True)
-
- #
- # Earily initialization
- #
- is_a_tty = sys.stdout.isatty() and sys.stderr.isatty()
-
- # Enable interactive mode if we're attached to a tty
- if main_options['no_interactive']:
- self.interactive = False
- else:
- self.interactive = is_a_tty
-
- # Handle errors interactively if we're in interactive mode
- # and --on-error was not specified on the command line
- if main_options.get('on_error') is not None:
- self._interactive_failures = False
- else:
- self._interactive_failures = self.interactive
-
- # Use color output if we're attached to a tty, unless
- # otherwise specified on the comand line
- if main_options['colors'] is None:
- self.colors = is_a_tty
- elif main_options['colors']:
- self.colors = True
- else:
- self.colors = False
-
- # create()
- #
- # Should be used instead of the regular constructor.
- #
- # This will select a platform specific App implementation
- #
- # Args:
- # The same args as the App() constructor
- #
- @classmethod
- def create(cls, *args, **kwargs):
- if sys.platform.startswith('linux'):
- # Use an App with linux specific features
- from .linuxapp import LinuxApp # pylint: disable=cyclic-import
- return LinuxApp(*args, **kwargs)
- else:
- # The base App() class is default
- return App(*args, **kwargs)
-
- # initialized()
- #
- # Context manager to initialize the application and optionally run a session
- # within the context manager.
- #
- # This context manager will take care of catching errors from within the
- # context and report them consistently, so the CLI need not take care of
- # reporting the errors and exiting with a consistent error status.
- #
- # Args:
- # session_name (str): The name of the session, or None for no session
- #
- # Note that the except_ argument may have a subtly different meaning depending
- # on the activity performed on the Pipeline. In normal circumstances the except_
- # argument excludes elements from the `elements` list. In a build session, the
- # except_ elements are excluded from the tracking plan.
- #
- # If a session_name is provided, we treat the block as a session, and print
- # the session header and summary, and time the main session from startup time.
- #
- @contextmanager
- def initialized(self, *, session_name=None):
- directory = self._main_options['directory']
- config = self._main_options['config']
-
- self._session_name = session_name
-
- #
- # Load the Context
- #
- try:
- self.context = Context(directory)
- self.context.load(config)
- except BstError as e:
- self._error_exit(e, "Error loading user configuration")
-
- # Override things in the context from our command line options,
- # the command line when used, trumps the config files.
- #
- override_map = {
- 'strict': '_strict_build_plan',
- 'debug': 'log_debug',
- 'verbose': 'log_verbose',
- 'error_lines': 'log_error_lines',
- 'message_lines': 'log_message_lines',
- 'on_error': 'sched_error_action',
- 'fetchers': 'sched_fetchers',
- 'builders': 'sched_builders',
- 'pushers': 'sched_pushers',
- 'network_retries': 'sched_network_retries',
- 'pull_buildtrees': 'pull_buildtrees',
- 'cache_buildtrees': 'cache_buildtrees'
- }
- for cli_option, context_attr in override_map.items():
- option_value = self._main_options.get(cli_option)
- if option_value is not None:
- setattr(self.context, context_attr, option_value)
- try:
- Platform.get_platform()
- except BstError as e:
- self._error_exit(e, "Error instantiating platform")
-
- # Create the logger right before setting the message handler
- self.logger = LogLine(self.context,
- self._content_profile,
- self._format_profile,
- self._success_profile,
- self._error_profile,
- self._detail_profile,
- indent=INDENT)
-
- # Propagate pipeline feedback to the user
- self.context.set_message_handler(self._message_handler)
-
- # Preflight the artifact cache after initializing logging,
- # this can cause messages to be emitted.
- try:
- self.context.artifactcache.preflight()
- except BstError as e:
- self._error_exit(e, "Error instantiating artifact cache")
-
- #
- # Load the Project
- #
- try:
- self.project = Project(directory, self.context, cli_options=self._main_options['option'],
- default_mirror=self._main_options.get('default_mirror'))
- except LoadError as e:
-
- # Help users that are new to BuildStream by suggesting 'init'.
- # We don't want to slow down users that just made a mistake, so
- # don't stop them with an offer to create a project for them.
- if e.reason == LoadErrorReason.MISSING_PROJECT_CONF:
- click.echo("No project found. You can create a new project like so:", err=True)
- click.echo("", err=True)
- click.echo(" bst init", err=True)
-
- self._error_exit(e, "Error loading project")
-
- except BstError as e:
- self._error_exit(e, "Error loading project")
-
- # Now that we have a logger and message handler,
- # we can override the global exception hook.
- sys.excepthook = self._global_exception_handler
-
- # Create the stream right away, we'll need to pass it around
- self.stream = Stream(self.context, self.project, self._session_start,
- session_start_callback=self.session_start_cb,
- interrupt_callback=self._interrupt_handler,
- ticker_callback=self._tick,
- job_start_callback=self._job_started,
- job_complete_callback=self._job_completed)
-
- # Create our status printer, only available in interactive
- self._status = Status(self.context,
- self._content_profile, self._format_profile,
- self._success_profile, self._error_profile,
- self.stream, colors=self.colors)
-
- # Mark the beginning of the session
- if session_name:
- self._message(MessageType.START, session_name)
-
- # Run the body of the session here, once everything is loaded
- try:
- yield
- except BstError as e:
-
- # Print a nice summary if this is a session
- if session_name:
- elapsed = self.stream.elapsed_time
-
- if isinstance(e, StreamError) and e.terminated: # pylint: disable=no-member
- self._message(MessageType.WARN, session_name + ' Terminated', elapsed=elapsed)
- else:
- self._message(MessageType.FAIL, session_name, elapsed=elapsed)
-
- # Notify session failure
- self._notify("{} failed".format(session_name), e)
-
- if self._started:
- self._print_summary()
-
- # Exit with the error
- self._error_exit(e)
- except RecursionError:
- click.echo("RecursionError: Dependency depth is too large. Maximum recursion depth exceeded.",
- err=True)
- sys.exit(-1)
-
- else:
- # No exceptions occurred, print session time and summary
- if session_name:
- self._message(MessageType.SUCCESS, session_name, elapsed=self.stream.elapsed_time)
- if self._started:
- self._print_summary()
-
- # Notify session success
- self._notify("{} succeeded".format(session_name), "")
-
- # init_project()
- #
- # Initialize a new BuildStream project, either with the explicitly passed options,
- # or by starting an interactive session if project_name is not specified and the
- # application is running in interactive mode.
- #
- # Args:
- # project_name (str): The project name, must be a valid symbol name
- # format_version (int): The project format version, default is the latest version
- # element_path (str): The subdirectory to store elements in, default is 'elements'
- # force (bool): Allow overwriting an existing project.conf
- #
- def init_project(self, project_name, format_version=BST_FORMAT_VERSION, element_path='elements', force=False):
- directory = self._main_options['directory']
- directory = os.path.abspath(directory)
- project_path = os.path.join(directory, 'project.conf')
-
- try:
- # Abort if the project.conf already exists, unless `--force` was specified in `bst init`
- if not force and os.path.exists(project_path):
- raise AppError("A project.conf already exists at: {}".format(project_path),
- reason='project-exists')
-
- if project_name:
- # If project name was specified, user interaction is not desired, just
- # perform some validation and write the project.conf
- _yaml.assert_symbol_name(None, project_name, 'project name')
- self._assert_format_version(format_version)
- self._assert_element_path(element_path)
-
- elif not self.interactive:
- raise AppError("Cannot initialize a new project without specifying the project name",
- reason='unspecified-project-name')
- else:
- # Collect the parameters using an interactive session
- project_name, format_version, element_path = \
- self._init_project_interactive(project_name, format_version, element_path)
-
- # Create the directory if it doesnt exist
- try:
- os.makedirs(directory, exist_ok=True)
- except IOError as e:
- raise AppError("Error creating project directory {}: {}".format(directory, e)) from e
-
- # Create the elements sub-directory if it doesnt exist
- elements_path = os.path.join(directory, element_path)
- try:
- os.makedirs(elements_path, exist_ok=True)
- except IOError as e:
- raise AppError("Error creating elements sub-directory {}: {}"
- .format(elements_path, e)) from e
-
- # Dont use ruamel.yaml here, because it doesnt let
- # us programatically insert comments or whitespace at
- # the toplevel.
- try:
- with open(project_path, 'w') as f:
- f.write("# Unique project name\n" +
- "name: {}\n\n".format(project_name) +
- "# Required BuildStream format version\n" +
- "format-version: {}\n\n".format(format_version) +
- "# Subdirectory where elements are stored\n" +
- "element-path: {}\n".format(element_path))
- except IOError as e:
- raise AppError("Error writing {}: {}".format(project_path, e)) from e
-
- except BstError as e:
- self._error_exit(e)
-
- click.echo("", err=True)
- click.echo("Created project.conf at: {}".format(project_path), err=True)
- sys.exit(0)
-
- # shell_prompt():
- #
- # Creates a prompt for a shell environment, using ANSI color codes
- # if they are available in the execution context.
- #
- # Args:
- # element (Element): The Element object to resolve a prompt for
- #
- # Returns:
- # (str): The formatted prompt to display in the shell
- #
- def shell_prompt(self, element):
- _, key, dim = element._get_display_key()
- element_name = element._get_full_name()
-
- if self.colors:
- prompt = self._format_profile.fmt('[') + \
- self._content_profile.fmt(key, dim=dim) + \
- self._format_profile.fmt('@') + \
- self._content_profile.fmt(element_name) + \
- self._format_profile.fmt(':') + \
- self._content_profile.fmt('$PWD') + \
- self._format_profile.fmt(']$') + ' '
- else:
- prompt = '[{}@{}:${{PWD}}]$ '.format(key, element_name)
-
- return prompt
-
- # cleanup()
- #
- # Cleans up application state
- #
- # This is called by Click at exit time
- #
- def cleanup(self):
- if self.stream:
- self.stream.cleanup()
-
- ############################################################
- # Abstract Class Methods #
- ############################################################
-
- # notify()
- #
- # Notify the user of something which occurred, this
- # is intended to grab attention from the user.
- #
- # This is guaranteed to only be called in interactive mode
- #
- # Args:
- # title (str): The notification title
- # text (str): The notification text
- #
- def notify(self, title, text):
- pass
-
- ############################################################
- # Local Functions #
- ############################################################
-
- # Local function for calling the notify() virtual method
- #
- def _notify(self, title, text):
- if self.interactive:
- self.notify(str(title), str(text))
-
- # Local message propagator
- #
- def _message(self, message_type, message, **kwargs):
- args = dict(kwargs)
- self.context.message(
- Message(None, message_type, message, **args))
-
- # Exception handler
- #
- def _global_exception_handler(self, etype, value, tb):
-
- # Print the regular BUG message
- formatted = "".join(traceback.format_exception(etype, value, tb))
- self._message(MessageType.BUG, str(value),
- detail=formatted)
-
- # If the scheduler has started, try to terminate all jobs gracefully,
- # otherwise exit immediately.
- if self.stream.running:
- self.stream.terminate()
- else:
- sys.exit(-1)
-
- #
- # Render the status area, conditional on some internal state
- #
- def _maybe_render_status(self):
-
- # If we're suspended or terminating, then dont render the status area
- if self._status and self.stream and \
- not (self.stream.suspended or self.stream.terminated):
- self._status.render()
-
- #
- # Handle ^C SIGINT interruptions in the scheduling main loop
- #
- def _interrupt_handler(self):
-
- # Only handle ^C interactively in interactive mode
- if not self.interactive:
- self._status.clear()
- self.stream.terminate()
- return
-
- # Here we can give the user some choices, like whether they would
- # like to continue, abort immediately, or only complete processing of
- # the currently ongoing tasks. We can also print something more
- # intelligent, like how many tasks remain to complete overall.
- with self._interrupted():
- click.echo("\nUser interrupted with ^C\n" +
- "\n"
- "Choose one of the following options:\n" +
- " (c)ontinue - Continue queueing jobs as much as possible\n" +
- " (q)uit - Exit after all ongoing jobs complete\n" +
- " (t)erminate - Terminate any ongoing jobs and exit\n" +
- "\n" +
- "Pressing ^C again will terminate jobs and exit\n",
- err=True)
-
- try:
- choice = click.prompt("Choice:",
- value_proc=_prefix_choice_value_proc(['continue', 'quit', 'terminate']),
- default='continue', err=True)
- except click.Abort:
- # Ensure a newline after automatically printed '^C'
- click.echo("", err=True)
- choice = 'terminate'
-
- if choice == 'terminate':
- click.echo("\nTerminating all jobs at user request\n", err=True)
- self.stream.terminate()
- else:
- if choice == 'quit':
- click.echo("\nCompleting ongoing tasks before quitting\n", err=True)
- self.stream.quit()
- elif choice == 'continue':
- click.echo("\nContinuing\n", err=True)
-
- def _tick(self, elapsed):
- self._maybe_render_status()
-
- def _job_started(self, job):
- self._status.add_job(job)
- self._maybe_render_status()
-
- def _job_completed(self, job, status):
- self._status.remove_job(job)
- self._maybe_render_status()
-
- # Dont attempt to handle a failure if the user has already opted to
- # terminate
- if status == JobStatus.FAIL and not self.stream.terminated:
-
- if isinstance(job, ElementJob):
- element = job.element
- queue = job.queue
-
- # Get the last failure message for additional context
- failure = self._fail_messages.get(element._unique_id)
-
- # XXX This is dangerous, sometimes we get the job completed *before*
- # the failure message reaches us ??
- if not failure:
- self._status.clear()
- click.echo("\n\n\nBUG: Message handling out of sync, " +
- "unable to retrieve failure message for element {}\n\n\n\n\n"
- .format(element), err=True)
- else:
- self._handle_failure(element, queue, failure)
- else:
- click.echo("\nTerminating all jobs\n", err=True)
- self.stream.terminate()
-
- def _handle_failure(self, element, queue, failure):
-
- # Handle non interactive mode setting of what to do when a job fails.
- if not self._interactive_failures:
-
- if self.context.sched_error_action == 'terminate':
- self.stream.terminate()
- elif self.context.sched_error_action == 'quit':
- self.stream.quit()
- elif self.context.sched_error_action == 'continue':
- pass
- return
-
- # Interactive mode for element failures
- with self._interrupted():
-
- summary = ("\n{} failure on element: {}\n".format(failure.action_name, element.name) +
- "\n" +
- "Choose one of the following options:\n" +
- " (c)ontinue - Continue queueing jobs as much as possible\n" +
- " (q)uit - Exit after all ongoing jobs complete\n" +
- " (t)erminate - Terminate any ongoing jobs and exit\n" +
- " (r)etry - Retry this job\n")
- if failure.logfile:
- summary += " (l)og - View the full log file\n"
- if failure.sandbox:
- summary += " (s)hell - Drop into a shell in the failed build sandbox\n"
- summary += "\nPressing ^C will terminate jobs and exit\n"
-
- choices = ['continue', 'quit', 'terminate', 'retry']
- if failure.logfile:
- choices += ['log']
- if failure.sandbox:
- choices += ['shell']
-
- choice = ''
- while choice not in ['continue', 'quit', 'terminate', 'retry']:
- click.echo(summary, err=True)
-
- self._notify("BuildStream failure", "{} on element {}"
- .format(failure.action_name, element.name))
-
- try:
- choice = click.prompt("Choice:", default='continue', err=True,
- value_proc=_prefix_choice_value_proc(choices))
- except click.Abort:
- # Ensure a newline after automatically printed '^C'
- click.echo("", err=True)
- choice = 'terminate'
-
- # Handle choices which you can come back from
- #
- if choice == 'shell':
- click.echo("\nDropping into an interactive shell in the failed build sandbox\n", err=True)
- try:
- prompt = self.shell_prompt(element)
- self.stream.shell(element, Scope.BUILD, prompt, isolate=True, usebuildtree='always')
- except BstError as e:
- click.echo("Error while attempting to create interactive shell: {}".format(e), err=True)
- elif choice == 'log':
- with open(failure.logfile, 'r') as logfile:
- content = logfile.read()
- click.echo_via_pager(content)
-
- if choice == 'terminate':
- click.echo("\nTerminating all jobs\n", err=True)
- self.stream.terminate()
- else:
- if choice == 'quit':
- click.echo("\nCompleting ongoing tasks before quitting\n", err=True)
- self.stream.quit()
- elif choice == 'continue':
- click.echo("\nContinuing with other non failing elements\n", err=True)
- elif choice == 'retry':
- click.echo("\nRetrying failed job\n", err=True)
- queue.failed_elements.remove(element)
- queue.enqueue([element])
-
- #
- # Print the session heading if we've loaded a pipeline and there
- # is going to be a session
- #
- def session_start_cb(self):
- self._started = True
- if self._session_name:
- self.logger.print_heading(self.project,
- self.stream,
- log_file=self._main_options['log_file'],
- styling=self.colors)
-
- #
- # Print a summary of the queues
- #
- def _print_summary(self):
- click.echo("", err=True)
- self.logger.print_summary(self.stream,
- self._main_options['log_file'],
- styling=self.colors)
-
- # _error_exit()
- #
- # Exit with an error
- #
- # This will print the passed error to stderr and exit the program
- # with -1 status
- #
- # Args:
- # error (BstError): A BstError exception to print
- # prefix (str): An optional string to prepend to the error message
- #
- def _error_exit(self, error, prefix=None):
- click.echo("", err=True)
- main_error = str(error)
- if prefix is not None:
- main_error = "{}: {}".format(prefix, main_error)
-
- click.echo(main_error, err=True)
- if error.detail:
- indent = " " * INDENT
- detail = '\n' + indent + indent.join(error.detail.splitlines(True))
- click.echo(detail, err=True)
-
- sys.exit(-1)
-
- #
- # Handle messages from the pipeline
- #
- def _message_handler(self, message, context):
-
- # Drop status messages from the UI if not verbose, we'll still see
- # info messages and status messages will still go to the log files.
- if not context.log_verbose and message.message_type == MessageType.STATUS:
- return
-
- # Hold on to the failure messages
- if message.message_type in [MessageType.FAIL, MessageType.BUG] and message.unique_id is not None:
- self._fail_messages[message.unique_id] = message
-
- # Send to frontend if appropriate
- if self.context.silent_messages() and (message.message_type not in unconditional_messages):
- return
-
- if self._status:
- self._status.clear()
-
- text = self.logger.render(message)
- click.echo(text, color=self.colors, nl=False, err=True)
-
- # Maybe render the status area
- self._maybe_render_status()
-
- # Additionally log to a file
- if self._main_options['log_file']:
- click.echo(text, file=self._main_options['log_file'], color=False, nl=False)
-
- @contextmanager
- def _interrupted(self):
- self._status.clear()
- try:
- with self.stream.suspend():
- yield
- finally:
- self._maybe_render_status()
-
- # Some validation routines for project initialization
- #
- def _assert_format_version(self, format_version):
- message = "The version must be supported by this " + \
- "version of buildstream (0 - {})\n".format(BST_FORMAT_VERSION)
-
- # Validate that it is an integer
- try:
- number = int(format_version)
- except ValueError as e:
- raise AppError(message, reason='invalid-format-version') from e
-
- # Validate that the specified version is supported
- if number < 0 or number > BST_FORMAT_VERSION:
- raise AppError(message, reason='invalid-format-version')
-
- def _assert_element_path(self, element_path):
- message = "The element path cannot be an absolute path or contain any '..' components\n"
-
- # Validate the path is not absolute
- if os.path.isabs(element_path):
- raise AppError(message, reason='invalid-element-path')
-
- # Validate that the path does not contain any '..' components
- path = element_path
- while path:
- split = os.path.split(path)
- path = split[0]
- basename = split[1]
- if basename == '..':
- raise AppError(message, reason='invalid-element-path')
-
- # _init_project_interactive()
- #
- # Collect the user input for an interactive session for App.init_project()
- #
- # Args:
- # project_name (str): The project name, must be a valid symbol name
- # format_version (int): The project format version, default is the latest version
- # element_path (str): The subdirectory to store elements in, default is 'elements'
- #
- # Returns:
- # project_name (str): The user selected project name
- # format_version (int): The user selected format version
- # element_path (str): The user selected element path
- #
- def _init_project_interactive(self, project_name, format_version=BST_FORMAT_VERSION, element_path='elements'):
-
- def project_name_proc(user_input):
- try:
- _yaml.assert_symbol_name(None, user_input, 'project name')
- except LoadError as e:
- message = "{}\n\n{}\n".format(e, e.detail)
- raise UsageError(message) from e
- return user_input
-
- def format_version_proc(user_input):
- try:
- self._assert_format_version(user_input)
- except AppError as e:
- raise UsageError(str(e)) from e
- return user_input
-
- def element_path_proc(user_input):
- try:
- self._assert_element_path(user_input)
- except AppError as e:
- raise UsageError(str(e)) from e
- return user_input
-
- w = TextWrapper(initial_indent=' ', subsequent_indent=' ', width=79)
-
- # Collect project name
- click.echo("", err=True)
- click.echo(self._content_profile.fmt("Choose a unique name for your project"), err=True)
- click.echo(self._format_profile.fmt("-------------------------------------"), err=True)
- click.echo("", err=True)
- click.echo(self._detail_profile.fmt(
- w.fill("The project name is a unique symbol for your project and will be used "
- "to distinguish your project from others in user preferences, namspaceing "
- "of your project's artifacts in shared artifact caches, and in any case where "
- "BuildStream needs to distinguish between multiple projects.")), err=True)
- click.echo("", err=True)
- click.echo(self._detail_profile.fmt(
- w.fill("The project name must contain only alphanumeric characters, "
- "may not start with a digit, and may contain dashes or underscores.")), err=True)
- click.echo("", err=True)
- project_name = click.prompt(self._content_profile.fmt("Project name"),
- value_proc=project_name_proc, err=True)
- click.echo("", err=True)
-
- # Collect format version
- click.echo(self._content_profile.fmt("Select the minimum required format version for your project"), err=True)
- click.echo(self._format_profile.fmt("-----------------------------------------------------------"), err=True)
- click.echo("", err=True)
- click.echo(self._detail_profile.fmt(
- w.fill("The format version is used to provide users who build your project "
- "with a helpful error message in the case that they do not have a recent "
- "enough version of BuildStream supporting all the features which your "
- "project might use.")), err=True)
- click.echo("", err=True)
- click.echo(self._detail_profile.fmt(
- w.fill("The lowest version allowed is 0, the currently installed version of BuildStream "
- "supports up to format version {}.".format(BST_FORMAT_VERSION))), err=True)
-
- click.echo("", err=True)
- format_version = click.prompt(self._content_profile.fmt("Format version"),
- value_proc=format_version_proc,
- default=format_version, err=True)
- click.echo("", err=True)
-
- # Collect element path
- click.echo(self._content_profile.fmt("Select the element path"), err=True)
- click.echo(self._format_profile.fmt("-----------------------"), err=True)
- click.echo("", err=True)
- click.echo(self._detail_profile.fmt(
- w.fill("The element path is a project subdirectory where element .bst files are stored "
- "within your project.")), err=True)
- click.echo("", err=True)
- click.echo(self._detail_profile.fmt(
- w.fill("Elements will be displayed in logs as filenames relative to "
- "the element path, and similarly, dependencies must be expressed as filenames "
- "relative to the element path.")), err=True)
- click.echo("", err=True)
- element_path = click.prompt(self._content_profile.fmt("Element path"),
- value_proc=element_path_proc,
- default=element_path, err=True)
-
- return (project_name, format_version, element_path)
-
-
-#
-# Return a value processor for partial choice matching.
-# The returned values processor will test the passed value with all the item
-# in the 'choices' list. If the value is a prefix of one of the 'choices'
-# element, the element is returned. If no element or several elements match
-# the same input, a 'click.UsageError' exception is raised with a description
-# of the error.
-#
-# Note that Click expect user input errors to be signaled by raising a
-# 'click.UsageError' exception. That way, Click display an error message and
-# ask for a new input.
-#
-def _prefix_choice_value_proc(choices):
-
- def value_proc(user_input):
- remaining_candidate = [choice for choice in choices if choice.startswith(user_input)]
-
- if not remaining_candidate:
- raise UsageError("Expected one of {}, got {}".format(choices, user_input))
- elif len(remaining_candidate) == 1:
- return remaining_candidate[0]
- else:
- raise UsageError("Ambiguous input. '{}' can refer to one of {}".format(user_input, remaining_candidate))
-
- return value_proc