diff --git a/rockcraft/application.py b/rockcraft/application.py
index e3dc154e3..46387629b 100644
--- a/rockcraft/application.py
+++ b/rockcraft/application.py
@@ -34,6 +34,7 @@
ProjectClass=project.Project,
BuildPlannerClass=project.BuildPlanner,
source_ignore_patterns=["*.rock"],
+ docs_url="https://documentation.ubuntu.com/rockcraft/en/stable",
)
diff --git a/rockcraft/commands/extensions.py b/rockcraft/commands/extensions.py
index 9870bde96..aef8946d8 100644
--- a/rockcraft/commands/extensions.py
+++ b/rockcraft/commands/extensions.py
@@ -97,6 +97,7 @@ class ExpandExtensionsCommand(AppCommand, abc.ABC):
@overrides
def run(self, parsed_args: argparse.Namespace) -> None:
"""Print the project's specification with the extensions expanded."""
- project = Project.unmarshal(load_project(Path("rockcraft.yaml")))
+ project_path = Path("rockcraft.yaml")
+ project = Project.from_yaml_data(load_project(project_path), project_path)
emit.message(project.to_yaml()) # pylint: disable=no-member
diff --git a/rockcraft/models/project.py b/rockcraft/models/project.py
index 22c4a0eef..534c566ab 100644
--- a/rockcraft/models/project.py
+++ b/rockcraft/models/project.py
@@ -263,6 +263,11 @@ def get_build_plan(self) -> list[BuildInfo]:
return build_infos
+ @override
+ @classmethod
+ def model_reference_slug(cls) -> str | None:
+ return "/reference/rockcraft.yaml"
+
class Project(YamlModelMixin, BuildPlanner, BaseProject): # type: ignore[misc]
"""Rockcraft project definition."""
@@ -512,6 +517,11 @@ def unmarshal(cls, data: dict[str, Any]) -> Self:
return cls(**data)
+ @override
+ @classmethod
+ def model_reference_slug(cls) -> str | None:
+ return "/reference/rockcraft.yaml"
+
def load_project(filename: Path) -> dict[str, Any]:
"""Load and unmarshal the project YAML file.
diff --git a/tests/unit/commands/test_expand_extensions.py b/tests/unit/commands/test_expand_extensions.py
index 365ee8244..7f097c7df 100644
--- a/tests/unit/commands/test_expand_extensions.py
+++ b/tests/unit/commands/test_expand_extensions.py
@@ -14,14 +14,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import argparse
+import copy
+import re
import textwrap
from pathlib import Path
import pytest
+from craft_application import util, errors
+
from rockcraft import extensions
from rockcraft.commands import ExpandExtensionsCommand
-from tests.unit.testing.extensions import FULL_EXTENSION_YAML, FullExtension
+from tests.unit.testing.extensions import (
+ FULL_EXTENSION_YAML,
+ FullExtension,
+ FULL_EXTENSION_PROJECT,
+)
# The project with the extension (FullExtension) expanded
EXPECTED_EXPAND_EXTENSIONS = textwrap.dedent(
@@ -82,3 +90,27 @@ def test_expand_extensions(setup_extensions, emitter, new_dir):
cmd.run(argparse.Namespace())
emitter.assert_message(EXPECTED_EXPAND_EXTENSIONS)
+
+
+def test_expand_extensions_error(setup_extensions, new_dir):
+ wrong_yaml = copy.deepcopy(FULL_EXTENSION_PROJECT)
+
+ # Misconfigure the plugin
+ wrong_yaml["parts"]["foo"]["plugin"] = "nonexistent"
+
+ # Misconfigure a service
+ wrong_yaml["services"]["my-service"]["override"] = "invalid"
+
+ project_file = Path("rockcraft.yaml")
+ dumped = util.dump_yaml(wrong_yaml)
+ project_file.write_text(dumped)
+
+ expected_message = re.escape(
+ "Bad rockcraft.yaml content:\n"
+ "- plugin not registered: 'nonexistent' (in field 'parts.foo')\n"
+ "- unexpected value; permitted: 'merge', 'replace' (in field 'services.my-service.override')"
+ )
+
+ cmd = ExpandExtensionsCommand(None)
+ with pytest.raises(errors.CraftValidationError, match=expected_message):
+ cmd.run(argparse.Namespace())
diff --git a/tests/unit/testing/extensions.py b/tests/unit/testing/extensions.py
index fed18152b..e6b8d269c 100644
--- a/tests/unit/testing/extensions.py
+++ b/tests/unit/testing/extensions.py
@@ -15,9 +15,9 @@
# along with this program. If not, see .
"""Fake Extensions for use in tests."""
-import textwrap
from typing import Any
+from craft_application import util
from overrides import override
from rockcraft.extensions.extension import Extension
@@ -103,29 +103,23 @@ def get_parts_snippet(self) -> dict[str, Any]:
return {"full-extension/new-part": {"plugin": "nil", "source": None}}
-FULL_EXTENSION_YAML = textwrap.dedent(
- f"""
- name: project-with-extensions
- version: latest
- base: ubuntu@22.04
- summary: Project with extensions
- description: Project with extensions
- license: Apache-2.0
- platforms:
- amd64:
-
- extensions:
- - {FullExtension.NAME}
-
- parts:
- foo:
- plugin: nil
- stage-packages:
- - old-package
-
- services:
- my-service:
- command: foo
- override: merge
- """
-)
+FULL_EXTENSION_PROJECT = {
+ "name": "project-with-extensions",
+ "version": "latest",
+ "base": "ubuntu@22.04",
+ "summary": "Project with extensions",
+ "description": "Project with extensions",
+ "license": "Apache-2.0",
+ "platforms": {"amd64": None},
+ "extensions": [FullExtension.NAME],
+ "parts": {"foo": {"plugin": "nil", "stage-packages": ["old-package"]}},
+ "services": {
+ "my-service": {
+ "command": "foo",
+ "override": "merge",
+ }
+ },
+}
+
+
+FULL_EXTENSION_YAML = util.dump_yaml(FULL_EXTENSION_PROJECT)