Skip to content

Commit 438cfda

Browse files
committed
fix(linux): embed MIT AppStream metadata and copyright in native installers
1 parent ea997af commit 438cfda

5 files changed

Lines changed: 199 additions & 7 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ jobs:
182182
python scripts/verify_native_network.py --image "$image" --platform "${{ matrix.artifact }}"
183183
- name: Install Linux packaging tools
184184
if: runner.os == 'Linux'
185-
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot desktop-file-utils xvfb
185+
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot desktop-file-utils xvfb appstream
186186
- uses: actions/setup-dotnet@v5
187187
if: runner.os == 'Windows'
188188
with:

‎docs/INSTALLATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Download the five installer formats from [0.9.3-DEV build 188.1](https://github.
2626
| Fedora/openSUSE and other compatible RPM systems x64 | `syntaxlauncher-linux-x64.rpm` | Install with the distribution's package manager (`sudo dnf install ./syntaxlauncher-linux-x64.rpm` or `sudo zypper install ./syntaxlauncher-linux-x64.rpm`); remove package `syntaxlauncher`. |
2727
| Other compatible desktop Linux systems x64 | `syntaxlauncher-linux-x64.AppImage` | Mark executable and open; this is a single-file portable application, not a system package. Where FUSE is unavailable, `./syntaxlauncher-linux-x64.AppImage --appimage-extract-and-run` uses temporary extraction. |
2828

29-
Linux DEB/RPM install the launcher under `/opt/syntaxlauncher` and register a desktop menu entry. All formats contain the launcher's Java runtime; Minecraft Java remains managed separately. AppImage does not eliminate native compatibility requirements: this build uses the Ubuntu 24.04 x64 application baseline, requires a compatible glibc desktop/graphics stack, and does not promise Alpine/musl, all historical distributions or Linux ARM64 support. Fedora RPM installation/removal is tested in a Fedora 44 container; a container lifecycle test does not certify graphical gameplay. openSUSE and other distributions require their own native acceptance before being called verified.
29+
Linux DEB/RPM install the launcher under `/opt/syntaxlauncher` and register a desktop menu entry. Both installers include AppStream metadata under `/usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml` with MIT as the project licence, SyntaxDevTeam as developer, and official project, support and source-code URLs. Debian also includes a machine-readable MIT copyright file under `/usr/share/doc/syntaxlauncher/copyright`; RPM retains its MIT license field. AppImage includes the same component metadata with an AppImage-specific desktop-file reference. Linux CI checks the actual DEB/RPM/AppImage payloads and validates the metadata with `appstreamcli`. Software-center UI behaviour can additionally depend on distribution AppStream catalogs; KDE Discover presentation must be checked separately. All formats contain the launcher's Java runtime; Minecraft Java remains managed separately. AppImage does not eliminate native compatibility requirements: this build uses the Ubuntu 24.04 x64 application baseline, requires a compatible glibc desktop/graphics stack, and does not promise Alpine/musl, all historical distributions or Linux ARM64 support. Fedora RPM installation/removal is tested in a Fedora 44 container; a container lifecycle test does not certify graphical gameplay. openSUSE and other distributions require their own native acceptance before being called verified.
3030

3131
Installer SHA-256 hashes are published as `SHA256SUMS-installers.txt`. The first installer previews are unsigned; Apple notarization and trusted Windows publisher signatures remain open production gates. Installer builds and temporary mount tests do not certify native trust prompts or all clean-machine workflows.
3232

‎packaging/smoke-linux.sh‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,48 @@ test -x /opt/syntaxlauncher/bin/SyntaxLauncher || { dpkg -L syntaxlauncher; exit
2525
desktop_entry="$(find /usr/share/applications /usr/local/share/applications -iname '*syntaxlauncher*.desktop' -print -quit 2>/dev/null || true)"
2626
test -n "$desktop_entry"
2727
desktop-file-validate "$desktop_entry"
28+
metainfo="/usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml"
29+
test -r "$metainfo"
30+
appstreamcli validate --no-net "$metainfo"
31+
grep -q '<project_license>MIT</project_license>' "$metainfo"
32+
test -r /usr/share/doc/syntaxlauncher/copyright
33+
grep -q '^License: MITstartup /opt/syntaxlauncher/bin/SyntaxLauncher deb-startup
34+
sudo apt-get remove -y syntaxlauncher
35+
test ! -e /opt/syntaxlauncher/bin/SyntaxLauncher
36+
grep -qx keep-world "$XDG_DATA_HOME/syntaxlauncher/instances/smoke/saves/marker"
37+
grep -qx keep-settings "$XDG_CONFIG_HOME/syntaxlauncher/marker"
38+
# Inspect and install/remove the RPM on an actual RPM distribution, not with --nodeps.
39+
rpm -qpi "$packages/syntaxlauncher-linux-x64.rpm" > "$evidence/rpm-metadata.txt"
40+
grep -Eq '^License[[:space:]]*:[[:space:]]*MIT' "$evidence/rpm-metadata.txt"
41+
rpm -qlp "$packages/syntaxlauncher-linux-x64.rpm" > "$evidence/rpm-contents.txt"
42+
grep -q '/usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml' "$evidence/rpm-contents.txt"
43+
docker run --rm -v "$packages:/packages:ro" fedora:44 bash -euc '
44+
mkdir -p /root/.local/share/syntaxlauncher/instances/smoke/saves
45+
echo keep-world > /root/.local/share/syntaxlauncher/instances/smoke/saves/marker
46+
dnf install -y /packages/syntaxlauncher-linux-x64.rpm
47+
test -x /opt/syntaxlauncher/bin/SyntaxLauncher
48+
test -r /usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml
49+
grep -q "<project_license>MIT</project_license>" /usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml
50+
dnf remove -y syntaxlauncher
51+
test ! -e /opt/syntaxlauncher/bin/SyntaxLauncher
52+
grep -qx keep-world /root/.local/share/syntaxlauncher/instances/smoke/saves/marker
53+
' > "$evidence/fedora-rpm-lifecycle.log" 2>&1
54+
# Extraction needs no FUSE mount and checks the payload used on other distributions.
55+
chmod +x "$packages/syntaxlauncher-linux-x64.AppImage"
56+
pushd "$smoke_root" >/dev/null
57+
"$packages/syntaxlauncher-linux-x64.AppImage" --appimage-extract > "$evidence/appimage-extract.log"
58+
test -x squashfs-root/AppRun
59+
test -L squashfs-root/.DirIcon
60+
test -r squashfs-root/usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml
61+
grep -q '<project_license>MIT</project_license>' squashfs-root/usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml
62+
appstreamcli validate --no-net squashfs-root/usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml
63+
startup "$smoke_root/squashfs-root/AppRun" appimage-startup
64+
popd >/dev/null
65+
/usr/share/doc/syntaxlauncher/copyright
66+
dpkg-deb -c "$packages/syntaxlauncher-linux-x64.deb" > "$evidence/deb-contents.txt"
67+
grep -q 'usr/share/metainfo/pl.syntaxdevteam.syntaxlauncher.metainfo.xml' "$evidence/deb-contents.txt"
68+
grep -q 'usr/share/doc/syntaxlauncher/copyright' "$evidence/deb-contents.txt"
69+
dpkg-deb -f "$packages/syntaxlauncher-linux-x64.deb" > "$evidence/deb-control.txt"
2870
startup /opt/syntaxlauncher/bin/SyntaxLauncher deb-startup
2971
sudo apt-get remove -y syntaxlauncher
3072
test ! -e /opt/syntaxlauncher/bin/SyntaxLauncher

‎scripts/package_installers.py‎

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
ROOT = Path(__file__).resolve().parent.parent
2323
BRANDING = ROOT / 'launcher-desktop/src/main/resources/branding'
24+
APPSTREAM_ID = 'pl.syntaxdevteam.syntaxlauncher'
25+
APPSTREAM_FILENAME = APPSTREAM_ID + '.metainfo.xml'
26+
PACKAGED_DESKTOP_ID = 'syntaxlauncher-SyntaxLauncher.desktop'
27+
APPIMAGE_DESKTOP_ID = 'syntaxlauncher.desktop'
2428
WIX_VERSION = '7.0.0'
2529
UPGRADE_CODE = 'D64AD1BC-4B59-4C1C-A45C-ABBC92E21739'
2630
INSTALLERS = {
@@ -38,6 +42,82 @@
3842
UI_NS = NS + '/ui'
3943

4044

45+
def appstream_metadata(desktop_id):
46+
"""Stable component ID, with a launchable desktop file matching the target format."""
47+
if desktop_id not in (PACKAGED_DESKTOP_ID, APPIMAGE_DESKTOP_ID):
48+
raise ValueError('Unsupported desktop file ID')
49+
return f'''<?xml version="1.0" encoding="UTF-8"?>
50+
<component type="desktop-application">
51+
<id>{APPSTREAM_ID}</id>
52+
<metadata_license>CC0-1.0</metadata_license>
53+
<project_license>MIT</project_license>
54+
<name>SyntaxLauncher</name>
55+
<summary>Modern Minecraft Java Edition launcher</summary>
56+
<description>
57+
<p>SyntaxLauncher is an independent desktop launcher for Minecraft Java Edition.
58+
It manages game versions, mod loaders, isolated game profiles and game files.
59+
Microsoft sign-in and verification of Minecraft Java Edition ownership are
60+
required before starting the game.</p>
61+
</description>
62+
<developer id="pl.syntaxdevteam">
63+
<name>SyntaxDevTeam</name>
64+
</developer>
65+
<categories><category>Game</category></categories>
66+
<launchable type="desktop-id">{desktop_id}</launchable>
67+
<url type="homepage">https://syntaxdevteam.pl/p/syntaxlauncher</url>
68+
<url type="bugtracker">https://github.com/SyntaxDevTeam/SyntaxLauncher/issues</url>
69+
<url type="help">https://syntaxdevteam.pl/p/syntaxlauncher-zarzadzanie-usluga</url>
70+
<url type="vcs-browser">https://github.com/SyntaxDevTeam/SyntaxLauncher</url>
71+
</component>
72+
'''
73+
74+
75+
def debian_copyright():
76+
"""Machine-readable Debian copyright: SPDX MIT plus its exact source text."""
77+
lines = (ROOT / 'LICENSE').read_text(encoding='utf-8').splitlines()
78+
return (
79+
'Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n'
80+
'Upstream-Name: SyntaxLauncher\n'
81+
'Source: https://github.com/SyntaxDevTeam/SyntaxLauncher\n\n'
82+
'Files: *\n'
83+
'Copyright: 2026 SyntaxDevTeam\n'
84+
'License: MIT\n'
85+
+ '\n'.join((' ' + line) if line else ' .' for line in lines) + '\n'
86+
)
87+
88+
89+
def rpm_metainfo_template(spec, metadata_path):
90+
"""Stage metadata before jpackage's automatic RPM file-inventory generation."""
91+
marker = '(cd %{buildroot} && find . -path ./lib/systemd'
92+
if spec.count(marker) != 1:
93+
raise ValueError('Unsupported jpackage RPM template; inventory marker missing')
94+
source = str(metadata_path.resolve(strict=True))
95+
if any(ch in source for ch in ('"', '\n', '%')):
96+
raise ValueError('Unsafe RPM source path')
97+
commands = (
98+
'install -d -m 755 "%{buildroot}/usr/share/metainfo"\n'
99+
f'install -m 644 "{source}" "%{{buildroot}}/usr/share/metainfo/{APPSTREAM_FILENAME}"\n'
100+
)
101+
return spec.replace(marker, commands + marker, 1)
102+
103+
104+
def add_debian_metadata(package, metadata_path):
105+
"""Place AppStream/copyright in the DEB payload instead of a postinst side effect."""
106+
with tempfile.TemporaryDirectory(prefix='syntaxlauncher-deb-meta-') as temporary:
107+
work = Path(temporary)
108+
root = work / 'root'
109+
run(['dpkg-deb', '--raw-extract', package, root])
110+
metainfo = root / 'usr/share/metainfo' / APPSTREAM_FILENAME
111+
metainfo.parent.mkdir(parents=True, exist_ok=True)
112+
shutil.copyfile(metadata_path, metainfo)
113+
copyright_file = root / 'usr/share/doc/syntaxlauncher/copyright'
114+
copyright_file.parent.mkdir(parents=True, exist_ok=True)
115+
copyright_file.write_text(debian_copyright(), encoding='utf-8')
116+
rebuilt = work / 'syntaxlauncher.deb'
117+
run(['dpkg-deb', '--build', '--root-owner-group', root, rebuilt])
118+
os.replace(rebuilt, package)
119+
120+
41121
def numeric_version(version):
42122
parse_launcher_version('launcherVersion=' + version)
43123
return version.split('-')[0].split('+')[0]
@@ -194,6 +274,10 @@ def appdir(image, destination):
194274
shutil.copyfile(BRANDING / 'syntaxlauncher-icon.png', destination / 'syntaxlauncher.png')
195275
(destination / '.DirIcon').symlink_to('syntaxlauncher.png')
196276
shutil.copyfile(ROOT / 'LICENSE', destination / 'LICENSE')
277+
metainfo_directory = destination / 'usr/share/metainfo'
278+
metainfo_directory.mkdir(parents=True, exist_ok=True)
279+
(metainfo_directory / APPSTREAM_FILENAME).write_text(
280+
appstream_metadata(APPIMAGE_DESKTOP_ID), encoding='utf-8')
197281

198282

199283
def desktop_template(text, kind):
@@ -205,7 +289,7 @@ def desktop_template(text, kind):
205289
return text.replace(marker, marker + '\nmkdir -p /usr/share/applications /usr/share/desktop-directories /etc/xdg/menus/applications-merged\n', 1)
206290

207291

208-
def linux_resources(work):
292+
def linux_resources(work, metadata_path):
209293
executable = Path(shutil.which(jpackage_tool()) or jpackage_tool()).resolve()
210294
home = Path(os.environ.get('JAVA_HOME', str(executable.parent.parent)))
211295
module = home / 'jmods/jdk.jpackage.jmod'
@@ -224,12 +308,17 @@ def linux_resources(work):
224308
base = extracted / 'jdk.jpackage/jdk/jpackage/internal/resources'
225309
contents = {source: (base / source).read_text(encoding='utf-8') for _, source, _ in templates}
226310
for kind, source, target in templates:
227-
(resources / target).write_text(desktop_template(contents[source], kind), encoding='utf-8')
311+
template = desktop_template(contents[source], kind)
312+
if kind == 'rpm':
313+
template = rpm_metainfo_template(template, metadata_path)
314+
(resources / target).write_text(template, encoding='utf-8')
228315
return resources
229316

230317

231318
def linux(image, version, output, work):
232-
resources = linux_resources(work)
319+
metadata_path = work / APPSTREAM_FILENAME
320+
metadata_path.write_text(appstream_metadata(PACKAGED_DESKTOP_ID), encoding='utf-8')
321+
resources = linux_resources(work, metadata_path)
233322
for kind in ('deb', 'rpm'):
234323
folder = work / kind
235324
folder.mkdir()
@@ -247,7 +336,10 @@ def linux(image, version, output, work):
247336
files = list(folder.glob('*.' + kind))
248337
if len(files) != 1:
249338
raise ValueError('Expected exactly one ' + kind + ' package')
250-
shutil.copyfile(files[0], output / ('syntaxlauncher-linux-x64.' + kind))
339+
destination = output / ('syntaxlauncher-linux-x64.' + kind)
340+
shutil.copyfile(files[0], destination)
341+
if kind == 'deb':
342+
add_debian_metadata(destination, metadata_path)
251343
directory = work / 'SyntaxLauncher.AppDir'
252344
directory.mkdir()
253345
appdir(image, directory)

‎scripts/test_package_installers.py‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
from unittest.mock import patch
88
import xml.etree.ElementTree as ET
99

10-
from package_installers import (NS, UPGRADE_CODE, appdir, native_version, linux_release, desktop_template, validate_image, wix_source, windows)
10+
from package_installers import (NS, UPGRADE_CODE, APPSTREAM_ID, APPSTREAM_FILENAME,
11+
APPIMAGE_DESKTOP_ID, PACKAGED_DESKTOP_ID, appdir, appstream_metadata, add_debian_metadata,
12+
rpm_metainfo_template, native_version, linux_release, desktop_template, validate_image,
13+
wix_source, windows)
1114
from verify_installers import NAMES, verify
1215
from publish_preview import PLATFORMS, publish
1316

@@ -36,6 +39,58 @@ def test_linux_preview_revisions_sort_before_stable_and_distinguish_builds(self)
3639
with self.assertRaises(ValueError):
3740
linux_release('0.9.3-DEV', build, attempt)
3841

42+
def test_appstream_metadata_identifies_license_publisher_and_desktop_file(self):
43+
for desktop_id in (PACKAGED_DESKTOP_ID, APPIMAGE_DESKTOP_ID):
44+
root = ET.fromstring(appstream_metadata(desktop_id))
45+
self.assertEqual('desktop-application', root.attrib['type'])
46+
self.assertEqual(APPSTREAM_ID, root.findtext('id'))
47+
self.assertEqual('MIT', root.findtext('project_license'))
48+
self.assertEqual('CC0-1.0', root.findtext('metadata_license'))
49+
self.assertEqual('SyntaxDevTeam', root.findtext('developer/name'))
50+
self.assertEqual('Game', root.findtext('categories/category'))
51+
self.assertEqual(desktop_id, root.findtext('launchable'))
52+
self.assertEqual('desktop-id', root.find('launchable').attrib['type'])
53+
with self.assertRaises(ValueError):
54+
appstream_metadata('unrelated.desktop')
55+
56+
def test_rpm_metainfo_staging_is_before_file_list(self):
57+
with tempfile.TemporaryDirectory() as directory:
58+
meta = Path(directory) / APPSTREAM_FILENAME
59+
meta.write_text(appstream_metadata(PACKAGED_DESKTOP_ID))
60+
marker = '(cd %{buildroot} && find . -path ./lib/systemd'
61+
spec = '%install\nrm -rf %{buildroot}\n' + marker + '\n%files -f %{package_filelist}\n'
62+
result = rpm_metainfo_template(spec, meta)
63+
self.assertIn(APPSTREAM_FILENAME, result)
64+
self.assertLess(result.index('/usr/share/metainfo'), result.index(marker))
65+
with self.assertRaises(ValueError):
66+
rpm_metainfo_template('broken', meta)
67+
68+
@unittest.skipUnless(shutil.which('dpkg-deb'), 'dpkg-deb is unavailable')
69+
def test_debian_payload_has_mit_metadata_and_copyright(self):
70+
with tempfile.TemporaryDirectory() as directory:
71+
root = Path(directory)
72+
source = root / 'source'
73+
(source / 'DEBIAN').mkdir(parents=True)
74+
(source / 'DEBIAN/control').write_text(
75+
'Package: syntaxlauncher\nVersion: 0.9.3-1\nArchitecture: amd64\n'
76+
'Maintainer: SyntaxDevTeam <[email protected]>\n'
77+
'Description: Test package\n')
78+
archive = root / 'test.deb'
79+
subprocess.run(['dpkg-deb', '--build', '--root-owner-group', str(source), str(archive)],
80+
check=True, capture_output=True)
81+
metadata = root / APPSTREAM_FILENAME
82+
metadata.write_text(appstream_metadata(PACKAGED_DESKTOP_ID))
83+
add_debian_metadata(archive, metadata)
84+
extracted = root / 'extracted'
85+
subprocess.run(['dpkg-deb', '--raw-extract', str(archive), str(extracted)],
86+
check=True, capture_output=True)
87+
self.assertEqual('MIT', ET.parse(extracted / 'usr/share/metainfo' / APPSTREAM_FILENAME).findtext('project_license'))
88+
notice = (extracted / 'usr/share/doc/syntaxlauncher/copyright').read_text()
89+
self.assertIn('License: MIT', notice)
90+
self.assertIn('Permission is hereby granted', notice)
91+
self.assertEqual('syntaxlauncher', subprocess.check_output(
92+
['dpkg-deb', '-f', str(archive), 'Package'], text=True).strip())
93+
3994
def test_msi_has_stable_identity_full_payload_user_scope_and_menu(self):
4095
with tempfile.TemporaryDirectory() as temporary:
4196
root = Path(temporary)
@@ -101,6 +156,9 @@ def test_appdir_preserves_layout_permissions_and_arguments_in_paths_with_spaces(
101156
self.assertEqual('syntaxlauncher.png', os.readlink(target / '.DirIcon'))
102157
self.assertTrue((target / 'SyntaxLauncher/relative-link').is_symlink())
103158
self.assertIn('Categories=Game;', (target / 'syntaxlauncher.desktop').read_text())
159+
metadata = ET.parse(target / 'usr/share/metainfo' / APPSTREAM_FILENAME)
160+
self.assertEqual('MIT', metadata.findtext('project_license'))
161+
self.assertEqual(APPIMAGE_DESKTOP_ID, metadata.findtext('launchable'))
104162
(image / 'escape').symlink_to(root)
105163
with self.assertRaises(ValueError):
106164
validate_image(image, 'linux-x64')

0 commit comments

Comments
 (0)