def _rerun_step(self, *, step: steps.Step, part, progress, hint='', prerequisite_step: steps.Step=steps.STAGE): staged_state = self.config.get_project_state(steps.STAGE) primed_state = self.config.get_project_state(steps.PRIME) # We need to clean this step, but if it involves cleaning any steps # upon which other parts depend, those parts need to be marked as dirty # so they can run again, taking advantage of the new dependency. dirty_parts = mark_dependents_dirty(part.name, step, self.config) # First clean the step, then run it again part.clean(staged_state, primed_state, step) for current_step in [step] + step.next_steps(): self._dirty_reports[part.name][current_step] = None self._steps_run[part.name].discard(current_step) self._run_step( step=step, part=part, progress=progress, hint=hint, prerequisite_step=prerequisite_step) if dirty_parts: logger.warning( 'The following {} now out of date: {}'.format( formatting_utils.pluralize( dirty_parts, 'part is', 'parts are'), formatting_utils.humanize_list(dirty_parts, 'and')))
def should_step_run(self, part: pluginhandler.PluginHandler, step: steps.Step) -> bool: """Determine if a given step of a given part should run. :param pluginhandler.PluginHandler part: Part in question. :param steps.Step step: Step in question. :return: Whether or not step should run. :rtype: bool A given step should run if it: 1. Hasn't yet run 2. Is dirty 3. Is outdated 4. Either (1), (2), or (3) apply to any earlier steps in the part's lifecycle """ if (not self.has_step_run(part, step) or self.get_outdated_report(part, step) is not None or self.get_dirty_report(part, step) is not None): return True previous_step = step.previous_step() if previous_step: return self.should_step_run(part, previous_step) return False
def get_outdated_report(self, step: steps.Step) -> OutdatedReport: """Return an OutdatedReport class describing why the step is outdated. A step is considered to be outdated if an earlier step in the lifecycle has been run more recently, or if the source code changed on disk. This means the step needs to be updated by taking modified files from the previous step. This is in contrast to a "dirty" step, which must be cleaned and run again. :param steps.Step step: The step to be checked. :returns: OutdatedReport if the step is outdated, None otherwise. """ try: return getattr(self, "check_{}".format(step.name))() except AttributeError: with contextlib.suppress(errors.StepHasNotRunError): timestamp = self.step_timestamp(step) for previous_step in reversed(step.previous_steps()): # Has a previous step run since this one ran? Then this # step needs to be updated. with contextlib.suppress(errors.StepHasNotRunError): if timestamp < self.step_timestamp(previous_step): return OutdatedReport(previous_step_modified=previous_step) return None
def should_step_run( self, part: pluginhandler.PluginHandler, step: steps.Step ) -> bool: """Determine if a given step of a given part should run. :param pluginhandler.PluginHandler part: Part in question. :param steps.Step step: Step in question. :return: Whether or not step should run. :rtype: bool A given step should run if it: 1. Hasn't yet run 2. Is dirty 3. Is outdated 4. Either (1), (2), or (3) apply to any earlier steps in the part's lifecycle """ if ( not self.has_step_run(part, step) or self.get_outdated_report(part, step) is not None or self.get_dirty_report(part, step) is not None ): return True previous_step = step.previous_step() if previous_step: return self.should_step_run(part, previous_step) return False
def get_outdated_report(self, step: steps.Step) -> OutdatedReport: """Return an OutdatedReport class describing why the step is outdated. A step is considered to be outdated if an earlier step in the lifecycle has been run more recently, or if the source code changed on disk. This means the step needs to be updated by taking modified files from the previous step. This is in contrast to a "dirty" step, which must be cleaned and run again. :param steps.Step step: The step to be checked. :returns: OutdatedReport if the step is outdated, None otherwise. """ try: return getattr(self, "check_{}".format(step.name))() except AttributeError: with contextlib.suppress(errors.StepHasNotRunError): timestamp = self.step_timestamp(step) for previous_step in reversed(step.previous_steps()): # Has a previous step run since this one ran? Then this # step needs to be updated. with contextlib.suppress(errors.StepHasNotRunError): if timestamp < self.step_timestamp(previous_step): return OutdatedReport(previous_step_modified=previous_step) return None
def _execute( # noqa: C901 step: steps.Step, parts: str, pack_project: bool = False, output: str = None, shell: bool = False, shell_after: bool = False, destructive_mode: bool = False, **kwargs) -> "Project": # fmt: on provider = "host" if destructive_mode else None build_environment = env.BuilderEnvironmentConfig(force_provider=provider) project = get_project(is_managed_host=build_environment.is_managed_host, **kwargs) conduct_project_sanity_check(project) if build_environment.is_managed_host or build_environment.is_host: project_config = project_loader.load_config(project) lifecycle.execute(step, project_config, parts) if pack_project: _pack(project.prime_dir, output=output) else: build_provider_class = build_providers.get_provider_for( build_environment.provider) echo.info("Launching a VM.") with build_provider_class(project=project, echoer=echo) as instance: instance.mount_project() try: if shell: # shell means we want to do everything right up to the previous # step and then go into a shell instead of the requested step. # the "snap" target is a special snowflake that has not made its # way to be a proper step. previous_step = None if pack_project: previous_step = steps.PRIME elif step > steps.PULL: previous_step = step.previous_step() # steps.PULL is the first step, so we would directly shell into it. if previous_step: instance.execute_step(previous_step) elif pack_project: instance.pack_project(output=output) else: instance.execute_step(step) except Exception: if project.debug: instance.shell() else: echo.warning( "Run the same command again with --debug to shell into the environment " "if you wish to introspect this failure.") raise else: if shell or shell_after: instance.shell() return project
def _rerun_step(self, *, step: steps.Step, part, progress, hint=""): staged_state = self.config.get_project_state(steps.STAGE) primed_state = self.config.get_project_state(steps.PRIME) # First clean the step, then run it again part.clean(staged_state, primed_state, step) # Uncache this and later steps since we just cleaned them: their status # has changed for current_step in [step] + step.next_steps(): self._cache.clear_step(part, current_step) self._run_step(step=step, part=part, progress=progress, hint=hint)
def _rerun_step(self, *, step: steps.Step, part, progress, hint=""): staged_state = self.config.get_project_state(steps.STAGE) primed_state = self.config.get_project_state(steps.PRIME) # First clean the step, then run it again part.clean(staged_state, primed_state, step) # Uncache this and later steps since we just cleaned them: their status # has changed for current_step in [step] + step.next_steps(): self._cache.clear_step(part, current_step) self._run_step(step=step, part=part, progress=progress, hint=hint)
def run(self, step: steps.Step, part_names=None): if part_names: self.parts_config.validate(part_names) # self.config.all_parts is already ordered, let's not lose that # and keep using a list. parts = [p for p in self.config.all_parts if p.name in part_names] processed_part_names = part_names else: parts = self.config.all_parts processed_part_names = self.config.part_names with config.CLIConfig() as cli_config: for current_step in step.previous_steps() + [step]: if current_step == steps.STAGE: # XXX check only for collisions on the parts that have # already been built --elopio - 20170713 pluginhandler.check_for_collisions(self.config.all_parts) for part in parts: if self._dirty_reports[part.name][current_step]: self._handle_dirty( part, current_step, self._dirty_reports[part.name][current_step], cli_config) elif current_step in self._steps_run[part.name]: # By default, if a step has already run, don't run it # again. However, automatically clean and re-run the # step if all the following conditions apply: # # 1. The step is the exact step that was requested # (not an earlier one) # 2. The part was explicitly specified if (part_names and current_step == step and part.name in part_names): getattr(self, '_re{}'.format( current_step.name))(part) else: notify_part_progress( part, 'Skipping {}'.format(current_step.name), '(already ran)') else: getattr(self, '_run_{}'.format( current_step.name))(part) self._steps_run[part.name].add(current_step) self._create_meta(step, processed_part_names)
def run(self, step: steps.Step, part_names=None): if part_names: self.parts_config.validate(part_names) # self.config.all_parts is already ordered, let's not lose that # and keep using a list. parts = [p for p in self.config.all_parts if p.name in part_names] processed_part_names = part_names else: parts = self.config.all_parts processed_part_names = self.config.part_names with config.CLIConfig() as cli_config: for current_step in step.previous_steps() + [step]: if current_step == steps.STAGE: # XXX check only for collisions on the parts that have # already been built --elopio - 20170713 pluginhandler.check_for_collisions(self.config.all_parts) for part in parts: self._handle_step(part_names, part, step, current_step, cli_config) self._create_meta(step, processed_part_names)
def run(self, step: steps.Step, part_names=None): if part_names: self.parts_config.validate(part_names) # self.config.all_parts is already ordered, let's not lose that # and keep using a list. parts = [p for p in self.config.all_parts if p.name in part_names] processed_part_names = part_names else: parts = self.config.all_parts processed_part_names = self.config.part_names with config.CLIConfig() as cli_config: for current_step in step.previous_steps() + [step]: if current_step == steps.STAGE: # XXX check only for collisions on the parts that have # already been built --elopio - 20170713 pluginhandler.check_for_collisions(self.config.all_parts) for part in parts: self._handle_step(part_names, part, step, current_step, cli_config) self._create_meta(step, processed_part_names)
def _execute( # noqa: C901 step: steps.Step, parts: str, pack_project: bool = False, output: str = None, shell: bool = False, shell_after: bool = False, destructive_mode: bool = False, **kwargs) -> "Project": _clean_provider_error() provider = "host" if destructive_mode else None build_environment = env.BuilderEnvironmentConfig(force_provider=provider) project = get_project(is_managed_host=build_environment.is_managed_host, **kwargs) echo.wrapped("Using {!r}: Project assets will be " "searched for from the {!r} directory.".format( project.info.snapcraft_yaml_file_path, os.path.relpath(project._get_snapcraft_assets_dir(), project._project_dir), )) conduct_project_sanity_check(project) if build_environment.is_managed_host or build_environment.is_host: project_config = project_loader.load_config(project) lifecycle.execute(step, project_config, parts) if pack_project: _pack(project.prime_dir, output=output) else: build_provider_class = build_providers.get_provider_for( build_environment.provider) try: build_provider_class.ensure_provider() except build_providers.errors.ProviderNotFound as provider_error: if provider_error.prompt_installable: click.echo(str(provider_error)) if click.confirm("Would you like to install it now?"): build_provider_class.setup_provider(echoer=echo) else: raise provider_error else: raise provider_error echo.info("Launching a VM.") with build_provider_class(project=project, echoer=echo) as instance: instance.mount_project() try: if shell: # shell means we want to do everything right up to the previous # step and then go into a shell instead of the requested step. # the "snap" target is a special snowflake that has not made its # way to be a proper step. previous_step = None if pack_project: previous_step = steps.PRIME elif step > steps.PULL: previous_step = step.previous_step() # steps.PULL is the first step, so we would directly shell into it. if previous_step: instance.execute_step(previous_step) elif pack_project: instance.pack_project(output=output) else: instance.execute_step(step) except Exception: _retrieve_provider_error(instance) if project.debug: instance.shell() else: echo.warning( "Run the same command again with --debug to shell into the environment " "if you wish to introspect this failure.") raise else: if shell or shell_after: instance.shell() return project
def _execute( # noqa: C901 step: steps.Step, parts: str, pack_project: bool = False, output: Optional[str] = None, shell: bool = False, shell_after: bool = False, setup_prime_try: bool = False, **kwargs, ) -> "Project": # Cleanup any previous errors. _clean_provider_error() build_provider = get_build_provider(**kwargs) build_provider_flags = get_build_provider_flags(build_provider, **kwargs) apply_host_provider_flags(build_provider_flags) is_managed_host = build_provider == "managed-host" # Temporary fix to ignore target_arch. if kwargs.get("target_arch") is not None and build_provider in ["multipass", "lxd"]: echo.warning( "Ignoring '--target-arch' flag. This flag requires --destructive-mode and is unsupported with Multipass and LXD build providers." ) kwargs.pop("target_arch") project = get_project(is_managed_host=is_managed_host, **kwargs) conduct_project_sanity_check(project, **kwargs) project_path = pathlib.Path(project._project_dir) if project_path.name in ["build-aux", "snap"]: echo.warning( f"Snapcraft is running in directory {project_path.name!r}. If this is the snap assets directory, please run snapcraft from {project_path.parent}." ) if build_provider in ["host", "managed-host"]: project_config = project_loader.load_config(project) lifecycle.execute(step, project_config, parts) if pack_project: _pack( project.prime_dir, compression=project._snap_meta.compression, output=output, ) else: build_provider_class = build_providers.get_provider_for(build_provider) try: build_provider_class.ensure_provider() except build_providers.errors.ProviderNotFound as provider_error: if provider_error.prompt_installable: if echo.is_tty_connected() and echo.confirm( "Support for {!r} needs to be set up. " "Would you like to do it now?".format(provider_error.provider) ): build_provider_class.setup_provider(echoer=echo) else: raise provider_error else: raise provider_error with build_provider_class( project=project, echoer=echo, build_provider_flags=build_provider_flags ) as instance: try: if shell: # shell means we want to do everything right up to the previous # step and then go into a shell instead of the requested step. # the "snap" target is a special snowflake that has not made its # way to be a proper step. previous_step = None if pack_project: previous_step = steps.PRIME elif step > steps.PULL: previous_step = step.previous_step() # steps.PULL is the first step, so we would directly shell into it. if previous_step: instance.execute_step(previous_step) elif pack_project: instance.pack_project(output=output) elif setup_prime_try: instance.expose_prime() instance.execute_step(step) else: instance.execute_step(step) except Exception: _retrieve_provider_error(instance) if project.debug: instance.shell() else: echo.warning( "Run the same command again with --debug to shell into the environment " "if you wish to introspect this failure." ) raise else: if shell or shell_after: instance.shell() return project
def _execute( # noqa: C901 step: steps.Step, parts: str, pack_project: bool = False, output: Optional[str] = None, shell: bool = False, shell_after: bool = False, setup_prime_try: bool = False, **kwargs, ) -> "Project": # Cleanup any previous errors. _clean_provider_error() build_provider = get_build_provider(**kwargs) build_provider_flags = get_build_provider_flags(build_provider, **kwargs) apply_host_provider_flags(build_provider_flags) is_managed_host = build_provider == "managed-host" project = get_project(is_managed_host=is_managed_host, **kwargs) conduct_project_sanity_check(project, **kwargs) if build_provider in ["host", "managed-host"]: project_config = project_loader.load_config(project) lifecycle.execute(step, project_config, parts) if pack_project: _pack(project.prime_dir, output=output) else: build_provider_class = build_providers.get_provider_for(build_provider) try: build_provider_class.ensure_provider() except build_providers.errors.ProviderNotFound as provider_error: if provider_error.prompt_installable: if echo.is_tty_connected() and echo.confirm( "Support for {!r} needs to be set up. " "Would you like to do that it now?".format( provider_error.provider)): build_provider_class.setup_provider(echoer=echo) else: raise provider_error else: raise provider_error with build_provider_class( project=project, echoer=echo, build_provider_flags=build_provider_flags) as instance: instance.mount_project() try: if shell: # shell means we want to do everything right up to the previous # step and then go into a shell instead of the requested step. # the "snap" target is a special snowflake that has not made its # way to be a proper step. previous_step = None if pack_project: previous_step = steps.PRIME elif step > steps.PULL: previous_step = step.previous_step() # steps.PULL is the first step, so we would directly shell into it. if previous_step: instance.execute_step(previous_step) elif pack_project: instance.pack_project(output=output) elif setup_prime_try: instance.expose_prime() instance.execute_step(step) else: instance.execute_step(step) except Exception: _retrieve_provider_error(instance) if project.debug: instance.shell() else: echo.warning( "Run the same command again with --debug to shell into the environment " "if you wish to introspect this failure.") raise else: if shell or shell_after: instance.shell() return project
def _execute( # noqa: C901 step: steps.Step, parts: str, pack_project: bool = False, output: str = None, shell: bool = False, shell_after: bool = False, **kwargs ) -> "Project": # fmt: on if sys.platform == "darwin": default_provider = "multipass" else: default_provider = "host" build_environment = env.BuilderEnvironmentConfig(default=default_provider) project = get_project(is_managed_host=build_environment.is_managed_host, **kwargs) conduct_project_sanity_check(project) # When we are ready to pull the trigger we will trigger this when # project.info.base is set if build_environment.is_multipass: build_provider_class = build_providers.get_provider_for( build_environment.provider ) echo.info("Launching a VM.") with build_provider_class(project=project, echoer=echo) as instance: instance.mount_project() try: if shell: # shell means we want to do everything right up to the previous # step and then go into a shell instead of the requested step. # the "snap" target is a special snowflake that has not made its # way to be a proper step. previous_step = None if pack_project: previous_step = steps.PRIME elif step > steps.PULL: previous_step = step.previous_step() # steps.PULL is the first step, so we would directly shell into it. if previous_step: instance.execute_step(previous_step) elif pack_project: instance.pack_project(output=output) else: instance.execute_step(step) except Exception: if project.debug: instance.shell() else: echo.warning("Run the same command again with --debug to shell into the environment " "if you wish to introspect this failure.") raise else: if shell or shell_after: instance.shell() elif build_environment.is_managed_host or build_environment.is_host: project_config = project_loader.load_config(project) lifecycle.execute(step, project_config, parts) if pack_project: _pack(project.prime_dir, output=output) else: # containerbuild takes a snapcraft command name, not a step lifecycle.containerbuild(command=step.name, project=project, args=parts) if pack_project: _pack(project.prime_dir, output=output) return project
def _execute( # noqa: C901 step: steps.Step, parts: str, pack_project: bool = False, output: str = None, shell: bool = False, shell_after: bool = False, destructive_mode: bool = False, **kwargs) -> "Project": # fmt: on _clean_provider_error() provider = "host" if destructive_mode else None build_environment = env.BuilderEnvironmentConfig(force_provider=provider) try: conduct_build_environment_sanity_check(build_environment.provider) except MultipassMissingInstallableError as e: click.echo("You need multipass installed to build snaps " "(https://github.com/CanonicalLtd/multipass).") if click.confirm("Would you like to install it now?"): _install_multipass() else: raise errors.SnapcraftEnvironmentError( "multipass is required to continue.") from e project = get_project(is_managed_host=build_environment.is_managed_host, **kwargs) conduct_project_sanity_check(project) if build_environment.is_managed_host or build_environment.is_host: project_config = project_loader.load_config(project) lifecycle.execute(step, project_config, parts) if pack_project: _pack(project.prime_dir, output=output) else: build_provider_class = build_providers.get_provider_for( build_environment.provider) echo.info("Launching a VM.") with build_provider_class(project=project, echoer=echo) as instance: instance.mount_project() try: if shell: # shell means we want to do everything right up to the previous # step and then go into a shell instead of the requested step. # the "snap" target is a special snowflake that has not made its # way to be a proper step. previous_step = None if pack_project: previous_step = steps.PRIME elif step > steps.PULL: previous_step = step.previous_step() # steps.PULL is the first step, so we would directly shell into it. if previous_step: instance.execute_step(previous_step) elif pack_project: instance.pack_project(output=output) else: instance.execute_step(step) except Exception: _retrieve_provider_error(instance) if project.debug: instance.shell() else: echo.warning( "Run the same command again with --debug to shell into the environment " "if you wish to introspect this failure.") raise else: if shell or shell_after: instance.shell() return project