10000 Add SplitArtifacts=metainfo by bluca · Pull Request #3542 · systemd/mkosi · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add SplitArtifacts=metainfo #3542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
10000 Expand Up @@ -26,6 +26,7 @@
from contextlib import AbstractContextManager
from pathlib import Path
from typing import Any, Optional, Union, cast
from urllib.parse import urlparse

from mkosi.archive import can_extract_tar, extract_tar, make_cpio, make_tar
from mkosi.bootloader import (
Expand Down Expand Up @@ -343,12 +344,14 @@ def check_root_populated(context: Context) -> None:
)


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

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

osrelease_content = {}

for candidate in ["usr/lib/os-release", "usr/lib/initrd-release", "etc/os-release"]:
osrelease = context.root / candidate

Expand Down Expand Up @@ -385,9 +388,69 @@ def configure_os_release(context: Context) -> None:

newosrelease.rename(osrelease)

osrelease_content = read_env_file(osrelease)

if ArtifactOutput.os_release in context.config.split_artifacts:
shutil.copy(osrelease, context.staging / context.config.output_split_os_release)

# https://www.freedesktop.org/software/appstream/docs/sect-Metadata-OS.html
osid = context.config.appstream_id or osrelease_content.get("ID", "linux")
name = context.config.appstream_name or osrelease_content.get("NAME") or context.config.image_id or osid
if context.config.appstream_description:
description = context.config.appstream_description
else:
description = f"{osrelease_content.get('PRETTY_NAME', 'Image')} built with mkosi"
icon = context.config.appstream_icon
home_url = context.config.appstream_url or osrelease_content.get("HOME_URL")
if home_url:
url = urlparse(home_url)
if not url.netloc:
home_url = None
if context.config.appstream_id:
id = context.config.appstream_id
elif home_url:
url = urlparse(home_url)
netloc = url.netloc.split(".")
netloc.reverse()
if "www" in netloc:
netloc.remove("www")
id = ".".join(netloc)
id += f".{osid}"
else:
id = osid
Comment on lines +405 to +420
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's shorten it down to this

Suggested change
if home_url:
url = urlparse(home_url)
if not url.netloc:
home_url = None
if context.config.appstream_id:
id = context.config.appstream_id
elif home_url:
url = urlparse(home_url)
netloc = url.netloc.split(".")
netloc.reverse()
if "www" in netloc:
netloc.remove("www")
id = ".".join(netloc)
id += f".{osid}"
else:
id = osid
id = osid
if context.config.appstream_id:
id = context.config.appstream_id
elif home_url:
url = urllib.parse.urlparse(home_url)
if url.netloc:
netloc = url.netloc.removeprefix("www").split(".")
id = f"{'.'.join(reversed(netloc))}.{osid}"
else:
home_url = None

It's clearer and looking at the spec the intention seems to be the strip a leading www but not the first. The alternative interpretation would be to remove any www, but that doesn't seem reasonable to me.

timestamp = (
datetime.datetime.fromtimestamp(context.config.source_date_epoch, tz=datetime.timezone.utc)
if context.config.source_date_epoch is not None
else datetime.datetime.now(tz=datetime.timezone.utc)
).isoformat()

metainfo = '<?xml version="1.0" encoding="UTF-8"?>\n'
metainfo += '<component type="operating-system">\n'
metainfo += f" <id>{id}</id>\n"
metainfo += f" <name>{name}</name>\n"
metainfo += f" <summary>{osid} image built with mkosi</summary>\n"
metainfo += f" <description><p>{description}</p></description>\n"
if home_url:
metainfo += f' <url type="homepage">{home_url}</url>\n'
if icon:
metainfo += f' <icon type="remote">{icon}</icon>\n'
metainfo += " <metadata_license>FSFAP</metadata_license>\n"
metainfo += " <releases>\n"
metainfo += (
f' <release version="{context.config.image_version}" date="{timestamp}" type="development">\n'
)
metainfo += " <description></description>\n"
metainfo += " </release>\n"
metainfo += " </releases>\n"
metainfo += ' <bundle type="systemd" class="sysupdate"></bundle>\n'
metainfo += "</component>\n"
Comment on lines +427 to +446
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think that's pretty much an eyesore, but well, it's XML. Here's one last attempt to make this readable without sigil soup around it

Suggested change
metainfo = '<?xml version="1.0" encoding="UTF-8"?>\n'
metainfo += '<component type="operating-system">\n'
metainfo += f" <id>{id}</id>\n"
metainfo += f" <name>{name}</name>\n"
metainfo += f" <summary>{osid} image built with mkosi</summary>\n"
metainfo += f" <description><p>{description}</p></description>\n"
if home_url:
metainfo += f' <url type="homepage">{home_url}</url>\n'
if icon:
metainfo += f' <icon type="remote">{icon}</icon>\n'
metainfo += " <metadata_license>FSFAP</metadata_license>\n"
metainfo += " <releases>\n"
metainfo += (
f' <release version="{context.config.image_version}" date="{timestamp}" type="development">\n'
)
metainfo += " <description></description>\n"
metainfo += " </release>\n"
metainfo += " </releases>\n"
metainfo += ' <bundle type="systemd" class="sysupdate"></bundle>\n'
metainfo += "</component>\n"
metainfo = textwrap.dedent(
f"""\
<?xml version="1.0" encoding="UTF-8"?>
<component type="operating-system">
<id>{id}</id>
<name>{name}</name>
<summary>{osid} image built with mkosi</summary>
<description><p>{description}</p></description>
{f'<url type="homepage">{home_url}</url>' if home_url else ""}
{f'<icon type="remote">{icon}</icon>' if icon else ""}
<metadata_license>FSFAP</metadata_license>
<releases>
<release version="{context.config.image_version}" date="{timestamp}" type="development">
<description></description>
</release>
</releases>
<bundle type="systemd" class="sysupdate"></bundle>
</component>
"""
)

The only downside is, that the optional things home_url and icon become empty lines when unset. I think that's not unreasonable, since in XML it's syntactically insignificant whitespace.


metainto_out = context.root / f"usr/share/metainfo/{id}.metainfo.xml"
metainto_out.parent.mkdir(parents=True, exist_ok=True)
metainto_out.write_text(metainfo)
if ArtifactOutput.appstream_metainfo in context.config.split_artifacts:
(context.staging / context.config.output_split_appstream_metainfo).write_text(metainfo)


def configure_extension_release(context: Context) -> None:
if context.config.output_format not in (OutputFormat.sysext, OutputFormat.confext):
Expand Down Expand Up @@ -3921,7 +3984,7 @@ def build_image(context: Context) -> None:
fixup_vmlinuz_location(context)

configure_autologin(context)
configure_os_release(context)
configure_os_release_and_appstream(context)
configure_extension_release(context)
configure_initrd(context)
configure_ssh(context)
Expand Down
56 changes: 56 additions & 0 deletions mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ class ArtifactOutput(StrEnum):
pcrs = enum.auto()
roothash = enum.auto()
os_release = enum.auto()
appstream_metainfo = enum.auto()

@staticmethod
def compat_no() -> list["ArtifactOutput"]:
Expand Down Expand Up @@ -1986,6 +1987,13 @@ class Config:
machine: Optional[str]
forward_journal: Optional[Path]

appstream_id: Optional[str]
appstream_name: Optional[str]
appstream_summary: Optional[str]
appstream_description: Optional[str]
appstream_url: Optional[str]
appstream_icon: Optional[str]

vmm: Vmm
console: ConsoleMode
cpus: int
Expand Down Expand Up @@ -2132,6 +2140,10 @@ def output_split_roothash(self) -> str:
def output_split_os_release(self) -> str:
return f"{self.output}.osrelease"

@property
def output_split_appstream_metainfo(self) -> str:
return f"{self.output}.metainfo.xml"

@property
def output_nspawn_settings(self) -> str:
return f"{self.output}.nspawn"
Expand Down Expand Up @@ -2173,6 +2185,7 @@ def outputs(self) -> list[str]:
self.output_split_pcrs,
self.output_split_roothash,
self.output_split_os_release,
self.output_split_appstream_metainfo,
self.output_nspawn_settings,
self.output_checksum,
self.output_signature,
Expand Down Expand Up @@ -3977,6 +3990,43 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
default=ConfigFeature.auto,
help="Run systemd-storagetm as part of the serve verb",
),
ConfigSetting(
dest="appstream_id",
section="Content",
parse=config_parse_string,
help="'id' field for Appstream metainfo file",
),
ConfigSetting(
dest="appstream_name",
section="Content",
parse=config_parse_string,
help="'name' field for Appstream metainfo file",
),
ConfigSetting(
dest="appstream_summary",
section="Content",
parse=config_parse_string,
help="'summary' field for Appstream metainfo file",
),
ConfigSetting(
dest="appstream_description",
section="Content",
parse=config_parse_string,
help="'description' field for Appstream metainfo file",
),
ConfigSetting(
dest="appstream_url",
section="Content",
parse=config_parse_string,
help="'url' homepage field for Appstream metainfo file",
),
ConfigSetting(
dest="appstream_icon",
section="Content",
parse=config_parse_string,
default="https://brand.systemd.io/assets/svg/systemd-logomark.svg",
help="'icon' URL field for Appstream metainfo file",
),
]
SETTINGS_LOOKUP_BY_NAME = {name: s for s in SETTINGS for name in [s.name, *s.compat_names]}
SETTINGS_LOOKUP_BY_DEST = {s.dest: s for s in SETTINGS}
Expand Down Expand Up @@ -5033,6 +5083,12 @@ def summary(config: Config) -> str:
Make Initrd: {yes_no(config.make_initrd)}
SSH: {yes_no(config.ssh)}
SELinux Relabel: {config.selinux_relabel}
Appstream Metainfo ID: {none_to_none(config.appstream_id)}
Appstream Metainfo Name: {none_to_none(config.appstream_name)}
Appstream Metainfo Summary: {none_to_none(config.appstream_summary)}
Appstream Metainfo Description: {none_to_none(config.appstream_description)}
Appstream Metainfo URL: {none_to_none(config.appstream_url)}
Appstream Icon: {none_to_none(config.appstream_icon)}
"""

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

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

### [Validation] Section

`SecureBoot=`, `--secure-boot=`
Expand Down
2 changes: 1 addition & 1 deletion mkosi/resources/mkosi-obs/mkosi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ LocalMirror=file:///.build.binaries/
[Output]
OutputDirectory=
Checksum=yes
SplitArtifacts=pcrs,roothash,os-release
SplitArtifacts=pcrs,roothash,os-release,appstream-metainfo
CompressOutput=zstd

[Validation]
Expand Down
12 changes: 12 additions & 0 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ def test_config() -> None:
dump = textwrap.dedent(
"""\
{
"AppstreamDescription": null,
"AppstreamIcon": null,
"AppstreamId": null,
"AppstreamName": null,
"AppstreamSummary": null,
"AppstreamUrl": null,
"Architecture": "ia64",
"Autologin": false,
"BaseTrees": [
Expand Down Expand Up @@ -437,6 +443,12 @@ def test_config() -> None:
)

args = Config(
appstream_id=None,
appstream_name=None,
appstream_summary=None,
appstream_description=None,
appstream_url=None,
appstream_icon=None,
architecture=Architecture.ia64,
autologin=False,
base_trees=[Path("/hello/world")],
Expand Down
0