diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index c1915f4915..618984b1ce 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -26,7 +26,7 @@ from pydantic_yaml import YamlModel from snapcraft import errors -from snapcraft.projects import App, Project +from snapcraft.projects import App, Project, UniqueStrList from snapcraft.utils import get_ld_library_paths, process_version @@ -176,6 +176,41 @@ def get_content_dirs(self, installed_path: Path) -> Set[Path]: return content_dirs +class Links(_SnapMetadataModel): + """Metadata links used in snaps.""" + + contact: Optional[UniqueStrList] + donation: Optional[UniqueStrList] + issues: Optional[UniqueStrList] + source_code: Optional[UniqueStrList] + website: Optional[UniqueStrList] + + @staticmethod + def _normalize_value( + value: Optional[Union[str, UniqueStrList]] + ) -> Optional[List[str]]: + if isinstance(value, str): + value = [value] + return value + + @classmethod + def from_project(cls, project: Project) -> "Links": + """Create Links from a Project.""" + return cls( + contact=cls._normalize_value(project.contact), + donation=cls._normalize_value(project.donation), + issues=cls._normalize_value(project.issues), + source_code=cls._normalize_value(project.source_code), + website=cls._normalize_value(project.website), + ) + + def __bool__(self) -> bool: + """Return True if any of the Links attributes are set.""" + return any( + [self.contact, self.donation, self.issues, self.source_code, self.website] + ) + + class SnapMetadata(_SnapMetadataModel): """The snap.yaml model. @@ -210,6 +245,7 @@ class Config: layout: Optional[Dict[str, Dict[str, str]]] system_usernames: Optional[Dict[str, Any]] provenance: Optional[str] + links: Optional[Links] @classmethod def unmarshal(cls, data: Dict[str, Any]) -> "SnapMetadata": @@ -403,6 +439,8 @@ def write(project: Project, prime_dir: Path, *, arch: str): # project provided assumes and computed assumes total_assumes = sorted(project.assumes + list(assumes)) + links = Links.from_project(project) + snap_metadata = SnapMetadata( name=project.name, title=project.title, @@ -425,6 +463,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): layout=project.layout, system_usernames=project.system_usernames, provenance=project.provenance, + links=links if links else None, ) if project.passthrough: for name, value in project.passthrough.items(): diff --git a/tests/spread/core22/patchelf/classic-python/task.yaml b/tests/spread/core22/patchelf/classic-python/task.yaml new file mode 100644 index 0000000000..e28f835b2e --- /dev/null +++ b/tests/spread/core22/patchelf/classic-python/task.yaml @@ -0,0 +1,41 @@ +summary: Build and run a Python-based core22 classic snap + +# To ensure the patchelf fixes are correct, we run this test on focal systems. +systems: + - ubuntu-20.04 + - ubuntu-20.04-64 + - ubuntu-20.04-amd64 + - ubuntu-20.04-arm64 + - ubuntu-20.04-armhf + - ubuntu-20.04-s390x + - ubuntu-20.04-ppc64el + +prepare: | + # Clone the snapcraft-docs/python-ctypes-example + git clone https://github.com/snapcraft-docs/python-ctypes-example.git + cd python-ctypes-example + # A known "good" commit from "main" at the time of writing this test + git checkout 31939ef68d8c383b9202f2588a704b3271bae009 + + # Replace the existing snap command with a call to the provisioned python3 + sed -i 's|command: bin/test-ctypes.py|command: bin/python3|' snap/snapcraft.yaml + +execute: | + cd python-ctypes-example + + # Build the core22 snap + unset SNAPCRAFT_BUILD_ENVIRONMENT + snapcraft --use-lxd + + # Install the new snap + sudo snap install --classic --dangerous example-python-ctypes*.snap + + # Run the snap's command; success means patchelf correctly linked the Python + # interpreter to core22's libc. Failure would output things like: + # version `GLIBC_2.35' not found (required by /snap/example-python-ctypes/x1/bin/python3) + example-python-ctypes -c "import ctypes; print(ctypes.__file__)" | MATCH "/snap/example-python-ctypes/" + +restore: | + cd python-ctypes-example + snapcraft clean + rm -f ./*.snap diff --git a/tests/spread/general/metadata-links/task.yaml b/tests/spread/general/metadata-links/task.yaml index 47730acec2..41b2976613 100644 --- a/tests/spread/general/metadata-links/task.yaml +++ b/tests/spread/general/metadata-links/task.yaml @@ -6,10 +6,18 @@ environment: prepare: | snap install yq + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + set_base snapcraft.yaml + restore: | snapcraft clean rm -rf ./*.snap + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml snapcraft.yaml + execute: | # Create a snap to trigger `snap pack`. snapcraft diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index d04a8cd408..b5148863f5 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -124,6 +124,111 @@ def test_assumes(simple_project, new_dir): ) +def test_links_scalars(simple_project, new_dir): + snap_yaml.write( + simple_project( + contact="me@acme.com", + issues="https://hubhub.com/issues", + donation="https://moneyfornothing.com", + source_code="https://closed.acme.com", + website="https://acme.com", + ), + prime_dir=Path(new_dir), + arch="amd64", + ) + yaml_file = Path("meta/snap.yaml") + assert yaml_file.is_file() + + content = yaml_file.read_text() + assert content == textwrap.dedent( + """\ + name: mytest + version: 1.29.3 + summary: Single-line elevator pitch for your amazing snap + description: test-description + architectures: + - amd64 + base: core22 + apps: + app1: + command: bin/mytest + confinement: strict + grade: stable + environment: + LD_LIBRARY_PATH: ${SNAP_LIBRARY_PATH}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} + PATH: $SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH + links: + contact: + - me@acme.com + donation: + - https://moneyfornothing.com + issues: + - https://hubhub.com/issues + source-code: + - https://closed.acme.com + website: + - https://acme.com + """ + ) + + +def test_links_lists(simple_project, new_dir): + snap_yaml.write( + simple_project( + contact=[ + "me@acme.com", + "you@acme.com", + ], + issues=[ + "https://hubhub.com/issues", + "https://corner.com/issues", + ], + donation=["https://moneyfornothing.com", "https://prince.com"], + source_code="https://closed.acme.com", + website="https://acme.com", + ), + prime_dir=Path(new_dir), + arch="amd64", + ) + yaml_file = Path("meta/snap.yaml") + assert yaml_file.is_file() + + content = yaml_file.read_text() + assert content == textwrap.dedent( + """\ + name: mytest + version: 1.29.3 + summary: Single-line elevator pitch for your amazing snap + description: test-description + architectures: + - amd64 + base: core22 + apps: + app1: + command: bin/mytest + confinement: strict + grade: stable + environment: + LD_LIBRARY_PATH: ${SNAP_LIBRARY_PATH}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} + PATH: $SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH + links: + contact: + - me@acme.com + - you@acme.com + donation: + - https://moneyfornothing.com + - https://prince.com + issues: + - https://hubhub.com/issues + - https://corner.com/issues + source-code: + - https://closed.acme.com + website: + - https://acme.com + """ + ) + + @pytest.fixture def complex_project(): snapcraft_yaml = textwrap.dedent( @@ -1025,3 +1130,60 @@ def test_architectures_all(simple_project, new_dir): "${SNAP_LIBRARY_PATH}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}:" "$SNAP/lib:$SNAP/usr/lib\n" ) in content + + +############## +# Test Links # +############## + + +def test_links_for_scalars(simple_project): + project = simple_project( + contact="me@acme.com", + issues="https://hubhub.com/issues", + donation="https://moneyfornothing.com", + source_code="https://closed.acme.com", + website="https://acme.com", + ) + + links = snap_yaml.Links.from_project(project) + + assert links.contact == [project.contact] + assert links.issues == [project.issues] + assert links.donation == [project.donation] + assert links.source_code == [project.source_code] + assert links.website == [project.website] + + assert bool(links) is True + + +def test_links_for_lists(simple_project): + project = simple_project( + contact=[ + "me@acme.com", + "you@acme.com", + ], + issues=[ + "https://hubhub.com/issues", + "https://corner.com/issues", + ], + donation=["https://moneyfornothing.com", "https://prince.com"], + ) + + links = snap_yaml.Links.from_project(project) + + assert links.contact == project.contact + assert links.issues == project.issues + assert links.donation == project.donation + assert links.source_code is None + assert links.website is None + + assert bool(links) is True + + +def test_no_links(simple_project): + project = simple_project() + + links = snap_yaml.Links.from_project(project) + + assert bool(links) is False