diff --git a/snapcraft/internal/errors.py b/snapcraft/internal/errors.py index eb213d6fc5..05ea7541b4 100644 --- a/snapcraft/internal/errors.py +++ b/snapcraft/internal/errors.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from snapcraft import formatting_utils + class SnapcraftError(Exception): """Base class for all snapcraft exceptions. @@ -45,6 +47,57 @@ def __init__(self, step): super().__init__(step=step) +class StepOutdatedError(SnapcraftError): + + fmt = ( + 'The {step!r} step of {part!r} is out of date:\n' + '{report}' + 'In order to continue, please clean that part\'s {step!r} step ' + 'by running:\n' + 'snapcraft clean {parts_names} -s {step}\n' + ) + + def __init__(self, *, step, part, + dirty_properties=None, dirty_project_options=None, + dependents=None): + messages = [] + if dirty_properties: + humanized_properties = formatting_utils.humanize_list( + dirty_properties, 'and') + pluralized_connection = formatting_utils.pluralize( + dirty_properties, 'property appears', + 'properties appear') + messages.append( + 'The {} part {} to have changed.\n'.format( + humanized_properties, pluralized_connection)) + if dirty_project_options: + humanized_options = formatting_utils.humanize_list( + dirty_project_options, 'and') + pluralized_connection = formatting_utils.pluralize( + dirty_project_options, 'option appears', + 'options appear') + messages.append( + 'The {} project {} to have changed.\n'.format( + humanized_options, pluralized_connection)) + if dependents: + humanized_dependents = formatting_utils.humanize_list( + dependents, 'and') + pluralized_dependents = formatting_utils.pluralize( + dependents, "depends", "depend") + messages.append('The {0!r} step for {1!r} needs to be run again, ' + 'but {2} {3} on it.\n'.format( + step, + part, + humanized_dependents, + pluralized_dependents)) + parts_names = ['{!s}'.format(d) for d in sorted(dependents)] + else: + parts_names = [part] + super().__init__(step=step, part=part, + report=''.join(messages), + parts_names=' '.join(parts_names)) + + class SnapcraftEnvironmentError(SnapcraftError): fmt = '{message}' diff --git a/snapcraft/internal/lifecycle.py b/snapcraft/internal/lifecycle.py index a4de934f71..15854bfc3b 100644 --- a/snapcraft/internal/lifecycle.py +++ b/snapcraft/internal/lifecycle.py @@ -267,35 +267,10 @@ def _create_meta(self, step, part_names): def _handle_dirty(self, part, step, dirty_report): if step not in _STEPS_TO_AUTOMATICALLY_CLEAN_IF_DIRTY: - message_components = [ - 'The {!r} step of {!r} is out of date:\n'.format( - step, part.name)] - - if dirty_report.dirty_properties: - humanized_properties = formatting_utils.humanize_list( - dirty_report.dirty_properties, 'and') - pluralized_connection = formatting_utils.pluralize( - dirty_report.dirty_properties, 'property appears', - 'properties appear') - message_components.append( - 'The {} part {} to have changed.\n'.format( - humanized_properties, pluralized_connection)) - - if dirty_report.dirty_project_options: - humanized_options = formatting_utils.humanize_list( - dirty_report.dirty_project_options, 'and') - pluralized_connection = formatting_utils.pluralize( - dirty_report.dirty_project_options, 'option appears', - 'options appear') - message_components.append( - 'The {} project {} to have changed.\n'.format( - humanized_options, pluralized_connection)) - - message_components.append( - "In order to continue, please clean that part's {0!r} step " - "by running: snapcraft clean {1} -s {0}\n".format( - step, part.name)) - raise RuntimeError(''.join(message_components)) + raise errors.StepOutdatedError( + step=step, part=part.name, + dirty_properties=dirty_report.dirty_properties, + dirty_project_options=dirty_report.dirty_project_options) staged_state = self.config.get_project_state('stage') primed_state = self.config.get_project_state('prime') @@ -310,17 +285,8 @@ def _handle_dirty(self, part, step, dirty_report): for dependent in self.config.all_parts: if (dependent.name in dependents and not dependent.is_clean('build')): - humanized_parts = formatting_utils.humanize_list( - dependents, 'and') - pluralized_depends = formatting_utils.pluralize( - dependents, "depends", "depend") - - raise RuntimeError( - 'The {0!r} step for {1!r} needs to be run again, but ' - '{2} {3} upon it. Please clean the build ' - 'step of {2} first.'.format( - step, part.name, humanized_parts, - pluralized_depends)) + raise errors.StepOutdatedError(step=step, part=part.name, + dependents=dependents) part.clean(staged_state, primed_state, step, '(out of date)') diff --git a/snapcraft/tests/test_lifecycle.py b/snapcraft/tests/test_lifecycle.py index 8e673b23eb..b15fd30ce8 100644 --- a/snapcraft/tests/test_lifecycle.py +++ b/snapcraft/tests/test_lifecycle.py @@ -426,7 +426,7 @@ def _fake_dirty_report(self, step): with mock.patch.object(pluginhandler.PluginHandler, 'get_dirty_report', _fake_dirty_report): raised = self.assertRaises( - RuntimeError, + errors.StepOutdatedError, lifecycle.execute, 'stage', self.project_options, part_names=['part1']) @@ -442,9 +442,11 @@ def _fake_dirty_report(self, step): self.assertThat( str(raised), Equals( + "The 'stage' step of 'part1' is out of date:\n" "The 'stage' step for 'part1' needs to be run again, but " - "'part2' depends upon it. Please clean the build step of " - "'part2' first.")) + "'part2' depends on it.\n" + "In order to continue, please clean that part's 'stage' step " + "by running:\nsnapcraft clean part2 -s stage\n")) def test_dirty_stage_part_with_unbuilt_dependent(self): self.make_snapcraft_yaml("""parts: @@ -542,7 +544,7 @@ def _fake_dirty_report(self, step): with mock.patch.object(pluginhandler.PluginHandler, 'get_dirty_report', _fake_dirty_report): raised = self.assertRaises( - RuntimeError, + errors.StepOutdatedError, lifecycle.execute, 'build', self.project_options) @@ -555,7 +557,7 @@ def _fake_dirty_report(self, step): "The 'build' step of 'part1' is out of date:\n" "The 'bar' and 'foo' part properties appear to have changed.\n" "In order to continue, please clean that part's 'build' step " - "by running: snapcraft clean part1 -s build\n")) + "by running:\nsnapcraft clean part1 -s build\n")) def test_dirty_pull_raises(self): self.make_snapcraft_yaml("""parts: @@ -579,7 +581,7 @@ def _fake_dirty_report(self, step): with mock.patch.object(pluginhandler.PluginHandler, 'get_dirty_report', _fake_dirty_report): raised = self.assertRaises( - RuntimeError, + errors.StepOutdatedError, lifecycle.execute, 'pull', self.project_options) @@ -590,7 +592,7 @@ def _fake_dirty_report(self, step): "The 'pull' step of 'part1' is out of date:\n" "The 'bar' and 'foo' project options appear to have changed.\n" "In order to continue, please clean that part's 'pull' step " - "by running: snapcraft clean part1 -s pull\n")) + "by running:\nsnapcraft clean part1 -s pull\n")) @mock.patch.object(snapcraft.BasePlugin, 'enable_cross_compilation') @mock.patch('snapcraft.repo.Repo.install_build_packages') @@ -614,7 +616,7 @@ def test_pull_is_dirty_if_target_arch_changes( # re-pulled due to the change in target architecture and raise an # error. raised = self.assertRaises( - RuntimeError, + errors.StepOutdatedError, lifecycle.execute, 'pull', snapcraft.ProjectOptions( target_deb_arch='armhf')) @@ -628,7 +630,7 @@ def test_pull_is_dirty_if_target_arch_changes( "The 'pull' step of 'part1' is out of date:\n" "The 'deb_arch' project option appears to have changed.\n" "In order to continue, please clean that part's 'pull' step " - "by running: snapcraft clean part1 -s pull\n")) + "by running:\nsnapcraft clean part1 -s pull\n")) def test_prime_excludes_internal_snapcraft_dir(self): self.make_snapcraft_yaml("""parts: