Skip to content

Commit 67131e4

Browse files
committed
Add SplitArtifacts=appstream-metainfo
GNOME Software parses .metainfo.xml files to provide nice metadata for artifacts it downloads/updates. Follow the spec and add parameter for it, filling the content from os-release. https://www.freedesktop.org/software/appstream/docs/sect-Metadata-OS.html
1 parent bae79ab commit 67131e4

File tree

5 files changed

+139
-6
lines changed

5 files changed

+139
-6
lines changed

mkosi/__init__.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from contextlib import AbstractContextManager
2727
from pathlib import Path
2828
from typing import Any, Optional, Union, cast
29+
from urllib.parse import urlparse
2930

3031
from mkosi.archive import can_extract_tar, extract_tar, make_cpio, make_tar
3132
from mkosi.bootloader import (
@@ -343,12 +344,14 @@ def check_root_populated(context: Context) -> None:
343344
)
344345

345346

346-
def configure_os_release(context: Context) -> None:
347-
"""Write IMAGE_ID and IMAGE_VERSION to /usr/lib/os-release in the image."""
347+
def configure_os_release_and_appstream(context: Context) -> None:
348+
"""Write os-release and appstream metainfo in the image."""
348349

349350
if context.config.overlay or context.config.output_format.is_extension_image():
350351
return
351352

353+
osrelease_content = {}
354+
352355
for candidate in ["usr/lib/os-release", "usr/lib/initrd-release", "etc/os-release"]:
353356
osrelease = context.root / candidate
354357

@@ -385,9 +388,68 @@ def configure_os_release(context: Context) -> None:
385388

386389
newosrelease.rename(osrelease)
387390

391+
osrelease_content = read_env_file(osrelease)
392+
388393
if ArtifactOutput.os_release in context.config.split_artifacts:
389394
shutil.copy(osrelease, context.staging / context.config.output_split_os_release)
390395

396+
# https://www.freedesktop.org/software/appstream/docs/sect-Metadata-OS.html
397+
osid = context.config.appstream_id or osrelease_content.get("ID", "linux")
398+
name = context.config.appstream_name or osrelease_content.get("NAME") or context.config.image_id or osid
399+
if context.config.appstream_description:
400+
description = context.config.appstream_description
401+
else:
402+
description = f"{osrelease_content.get('PRETTY_NAME', 'Image')} built with mkosi"
403+
icon = context.config.appstream_icon or "https://brand.systemd.io/assets/svg/systemd-logomark.svg"
404+
home_url = context.config.appstream_url or osrelease_content.get("HOME_URL")
405+
if home_url:
406+
url = urlparse(home_url)
407+
if not url.netloc:
408+
home_url = None
409+
if context.config.appstream_id:
410+
id = context.config.appstream_id
411+
elif home_url:
412+
url = urlparse(home_url)
413+
netloc = url.netloc.split(".")
414+
netloc.reverse()
415+
if "www" in netloc:
416+
netloc.remove("www")
417+
id = ".".join(netloc)
418+
id += f".{osid}"
419+
else:
420+
id = osid
421+
timestamp = (
422+
datetime.datetime.fromtimestamp(context.config.source_date_epoch, tz=datetime.timezone.utc)
423+
if context.config.source_date_epoch is not None
424+
else datetime.datetime.now(tz=datetime.timezone.utc)
425+
).isoformat()
426+
427+
metainfo = '<?xml version="1.0" encoding="UTF-8"?>\n'
428+
metainfo += '<component type="operating-system">\n'
429+
metainfo += f" <id>{id}</id>\n"
430+
metainfo += f" <name>{name}</name>\n"
431+
metainfo += f" <summary>{osid} image built with mkosi</summary>\n"
432+
metainfo += f" <description><p>{description}</p></description>\n"
433+
if home_url:
434+
metainfo += f' <url type="homepage">{home_url}</url>\n'
435+
if icon:
436+
metainfo += f' <icon type="remote">{icon}</icon>\n'
437+
metainfo += " <metadata_license>FSFAP</metadata_license>\n"
438+
metainfo += " <releases>\n"
439+
metainfo += (
440+
f' <release version="{context.config.image_version}" date="{timestamp}" type="development">\n'
441+
)
442+
metainfo += " <description></description>\n"
443+
metainfo += " </release>\n"
444+
metainfo += " </releases>\n"
445+
metainfo += "</component>\n"
446+
447+
metainto_out = context.root / f"usr/share/metainfo/{id}.metainfo.xml"
448+
metainto_out.parent.mkdir(parents=True, exist_ok=True)
449+
metainto_out.write_text(metainfo)
450+
if ArtifactOutput.appstream_metainfo in context.config.split_artifacts:
451+
(context.staging / context.config.output_split_appstream_metainfo).write_text(metainfo)
452+
391453

392454
def configure_extension_release(context: Context) -> None:
393455
if context.config.output_format not in (OutputFormat.sysext, OutputFormat.confext):
@@ -3921,7 +3983,7 @@ def build_image(context: Context) -> None:
39213983
fixup_vmlinuz_location(context)
39223984

39233985
configure_autologin(context)
3924-
configure_os_release(context)
3986+
configure_os_release_and_appstream(context)
39253987
configure_extension_release(context)
39263988
configure_initrd(context)
39273989
configure_ssh(context)

mkosi/config.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ class ArtifactOutput(StrEnum):
575575
pcrs = enum.auto()
576576
roothash = enum.auto()
577577
os_release = enum.auto()
578+
appstream_metainfo = enum.auto()
578579

579580
@staticmethod
580581
def compat_no() -> list["ArtifactOutput"]:
@@ -1986,6 +1987,13 @@ class Config:
19861987
machine: Optional[str]
19871988
forward_journal: Optional[Path]
19881989

1990+
appstream_id: Optional[str]
1991+
appstream_name: Optional[str]
1992+
appstream_summary: Optional[str]
1993+
appstream_description: Optional[str]
1994+
appstream_url: Optional[str]
1995+
appstream_icon: Optional[str]
1996+
19891997
vmm: Vmm
19901998
console: ConsoleMode
19911999
cpus: int
@@ -2132,6 +2140,10 @@ def output_split_roothash(self) -> str:
21322140
def output_split_os_release(self) -> str:
21332141
return f"{self.output}.osrelease"
21342142

2143+
@property
2144+
def output_split_appstream_metainfo(self) -> str:
2145+
return f"{self.output}.metainfo.xml"
2146+
21352147
@property
21362148
def output_nspawn_settings(self) -> str:
21372149
return f"{self.output}.nspawn"
@@ -2173,6 +2185,7 @@ def outputs(self) -> list[str]:
21732185
self.output_split_pcrs,
21742186
self.output_split_roothash,
21752187
self.output_split_os_release,
2188+
self.output_split_appstream_metainfo,
21762189
self.output_nspawn_settings,
21772190
self.output_checksum,
21782191
self.output_signature,
@@ -3977,6 +3990,42 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
39773990
default=ConfigFeature.auto,
39783991
help="Run systemd-storagetm as part of the serve verb",
39793992
),
3993+
ConfigSetting(
3994+
dest="appstream_id",
3995+
section="Content",
3996+
parse=config_parse_string,
3997+
help="'id' field for Appstream metainfo file",
3998+
),
3999+
ConfigSetting(
4000+
dest="appstream_name",
4001+
section="Content",
4002+
parse=config_parse_string,
4003+
help="'name' field for Appstream metainfo file",
4004+
),
4005+
ConfigSetting(
4006+
dest="appstream_summary",
4007+
section="Content",
4008+
parse=config_parse_string,
4009+
help="'summary' field for Appstream metainfo file",
4010+
),
4011+
ConfigSetting(
4012+
dest="appstream_description",
4013+
section="Content",
4014+
parse=config_parse_string,
4015+
help="'description' field for Appstream metainfo file",
4016+
),
4017+
ConfigSetting(
4018+
dest="appstream_url",
4019+
section="Content",
4020+
parse=config_parse_string,
4021+
help="'url' homepage field for Appstream metainfo file",
4022+
),
4023+
ConfigSetting(
4024+
dest="appstream_icon",
4025+
section="Content",
4026+
parse=config_parse_string,
4027+
help="'icon' URL field for Appstream metainfo file",
4028+
),
39804029
]
39814030
SETTINGS_LOOKUP_BY_NAME = {name: s for s in SETTINGS for name in [s.name, *s.compat_names]}
39824031
SETTINGS_LOOKUP_BY_DEST = {s.dest: s for s in SETTINGS}
@@ -5033,6 +5082,12 @@ def summary(config: Config) -> str:
50335082
Make Initrd: {yes_no(config.make_initrd)}
50345083
SSH: {yes_no(config.ssh)}
50355084
SELinux Relabel: {config.selinux_relabel}
5085+
Appstream Metainfo ID: {none_to_none(config.appstream_id)}
5086+
Appstream Metainfo Name: {none_to_none(config.appstream_name)}
5087+
Appstream Metainfo Summary: {none_to_none(config.appstream_summary)}
5088+
Appstream Metainfo Description: {none_to_none(config.appstream_description)}
5089+
Appstream Metainfo URL: {none_to_none(config.appstream_url)}
5090+
Appstream Icon: {none_to_none(config.appstream_icon)}
50365091
"""
50375092

50385093
if config.output_format.is_extension_or_portable_image() or config.output_format in (

mkosi/resources/man/mkosi.1.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,8 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
624624
`SplitArtifacts=`, `--split-artifacts=`
625625
: The artifact types to split out of the final image. A comma-delimited
626626
list consisting of `uki`, `kernel`, `initrd`, `os-release`, `prcs`, `partitions`,
627-
`roothash` and `tar`. When building a bootable image `kernel` and `initrd`
628-
correspond to their artifact found in the image (or in the UKI),
627+
`appstream-metainfo`, `roothash` and `tar`. When building a bootable image `kernel`
628+
and `initrd` correspond to their artifact found in the image (or in the UKI),
629629
while `uki` copies out the entire UKI. If `pcrs` is specified, a JSON
630630
file containing the pre-calculated TPM2 digests is written out, according
631631
to the [UKI specification](https://uapi-group.org/specifications/specs/unified_kernel_image/#json-format-for-pcrsig),
@@ -1156,6 +1156,10 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
11561156
`mkosi.machine-id` exists in the local directory, the UUID to use is
11571157
read from it. Otherwise, `uninitialized` will be written to `/etc/machine-id`.
11581158

1159+
`AppstreamId=`, `--appstream-id=`, `AppstreamName=`, `--appstream-name=`, `AppstreamSummary=`, `--appstream-summary=`, `AppstreamDescription=`, `--appstream-description=`, `AppstreamUrl=`, `--appstream-url=`, `AppstreamIcon=`, `--appstream-icon=`
1160+
: Specifies content for the [appstream metainfo](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-OS.html)
1161+
file. If not provided, the content will be derived from the os-release file.
1162+
11591163
### [Validation] Section
11601164

11611165
`SecureBoot=`, `--secure-boot=`

mkosi/resources/mkosi-obs/mkosi.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ LocalMirror=file:///.build.binaries/
1414
[Output]
1515
OutputDirectory=
1616
Checksum=yes
17-
SplitArtifacts=pcrs,roothash
17+
SplitArtifacts=pcrs,roothash,appstream-metainfo
1818
CompressOutput=zstd
1919

2020
[Validation]

tests/test_json.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ def test_config() -> None:
9898
dump = textwrap.dedent(
9999
"""\
100100
{
101+
"AppstreamDescription": null,
102+
"AppstreamIcon": null,
103+
"AppstreamId": null,
104+
"AppstreamName": null,
105+
"AppstreamSummary": null,
106+
"AppstreamUrl": null,
101107
"Architecture": "ia64",
102108
"Autologin": false,
103109
"BaseTrees": [
@@ -437,6 +443,12 @@ def test_config() -> None:
437443
)
438444

439445
args = Config(
446+
appstream_id=None,
447+
appstream_name=None,
448+
appstream_summary=None,
449+
appstream_description=None,
450+
appstream_url=None,
451+
appstream_icon=None,
440452
architecture=Architecture.ia64,
441453
autologin=False,
442454
base_trees=[Path("/hello/world")],

0 commit comments

Comments
 (0)