├── flatpak_builder_lint ├── staticfiles │ └── __init__.py ├── __init__.py ├── checks │ ├── __init__.py │ ├── jsonschema.py │ ├── eolruntime.py │ ├── reposize.py │ ├── elfarch.py │ ├── toplevel.py │ ├── flathub_json.py │ ├── modules.py │ ├── metainfo.py │ ├── appid.py │ ├── flatmanager.py │ └── screenshots.py ├── config.py ├── gitutils.py ├── appstream.py ├── ostree.py ├── builddir.py ├── exceptions_janitor.py └── manifest.py ├── tests ├── manifests │ ├── symlinks │ │ ├── symlink.json │ │ └── source.json │ ├── appid.json │ ├── appid-too-few-cpts.json │ ├── exceptions.json │ ├── unknown-properties-1.json │ ├── git-repo-checks │ │ ├── git-repo-checks.json │ │ └── .gitmodules │ ├── unknown-properties-2.json │ ├── exceptions_wildcard.json │ ├── finish_args_missing.json │ ├── finish_args_empty.json │ ├── eol_runtime.json │ ├── display-supported3.json │ ├── display-only-wayland.json │ ├── home_host │ │ ├── org.flathub.home_host2.json │ │ ├── org.flathub.home_host3.json │ │ ├── org.flathub.home_host4.json │ │ ├── org.flathub.home_host5.json │ │ ├── org.flathub.home_host6.json │ │ ├── org.flathub.home_host7.json │ │ ├── org.flathub.home_host1.json │ │ └── org.flathub.home_host_false.json │ ├── finish_args-wayland-x11.json │ ├── finish_args-incorrect_secrets-talk-name.json │ ├── toplevel.json │ ├── display-supported2.json │ ├── base_app.json │ ├── finish_args-new-metadata.json │ ├── flathub.json │ ├── domain_checks │ │ ├── com.github.json │ │ ├── ch.wwwwww.bar.json │ │ ├── io.github.ghost.bar.json │ │ ├── com.--github--.flathub.json │ │ ├── org.eu.encom.spectral.json │ │ ├── io.frama.flopedt.FlOpEDT.json │ │ ├── io.github.flatpak.flatpak.json │ │ ├── io.sourceforge.xampp.bar.json │ │ ├── io.frama.wwwwwwwwwwwww.bar.json │ │ ├── io.github.wwwwwwwwwwwww.bar.json │ │ ├── io.gitlab.deeplomatics.bar.json │ │ ├── io.gitlab.wwwwwwwwwwwww.bar.json │ │ ├── org.gnome.design.VectorSlicer.json │ │ ├── io.github.wwwwwwwwwwwww.foo.bar.json │ │ ├── org.gnome.gitlab.YaLTeR.Identity.json │ │ ├── page.codeberg.wwwwwwwwwwwww.foo.json │ │ ├── io.sourceforge.wwwwwwwwwwwwwwww.bar.json │ │ ├── org.gnome.gitlab.wwwwwwwwwwwww.bar.json │ │ ├── org.gnome.gitlab.user.project.foo.bar.json │ │ ├── page.codeberg.forgejo.code-of-conduct.json │ │ ├── org.freedesktop.gitlab.wwwwwwwwwwwww.bar.json │ │ ├── org.gtk.Gtk33theme.Helium-dark.json │ │ ├── org.electronjs.Electron200.BaseApp.json │ │ └── org.freedesktop.gitlab.drm_hwcomposer.drm-hwcomposer.json │ ├── yaml │ │ ├── manfiest-valid.yml │ │ └── manfiest-invalid.yml │ ├── flathub_json.json │ ├── dconf.json │ ├── com.example.json_warnings.json │ ├── finish_args_fs_negative.json │ ├── module-nightly-x-checker.json │ ├── xdg-dirs-access.json │ ├── modules.json │ ├── network_access.json │ ├── modules_git_disallowed.json │ ├── modules_git_allowed.json │ └── finish_args.json ├── builddir │ ├── appstream-no-icon-file │ │ ├── org.flathub.appstream_no_icon_file.desktop │ │ ├── metadata │ │ ├── org.flathub.appstream_no_icon_file.metainfo.xml │ │ └── org.flathub.appstream_no_icon_file.xml │ ├── appstream-broken-remote-icon │ │ ├── org.flathub.appstream_broken_remote_icon.desktop │ │ ├── metadata │ │ ├── org.flathub.appstream_broken_remote_icon.metainfo.xml │ │ └── org.flathub.appstream_broken_remote_icon.xml │ ├── appstream-icon-key-no-type │ │ ├── org.flathub.appstream_icon_key_no_type.desktop │ │ ├── metadata │ │ ├── org.flathub.appstream_icon_key_no_type.metainfo.xml │ │ └── org.flathub.appstream_icon_key_no_type.xml │ ├── appid │ │ └── metadata │ ├── console │ │ ├── metadata │ │ ├── org.flathub.example.console.desktop │ │ ├── org.flathub.example.console.metainfo.xml │ │ └── org.flathub.example.console.xml │ ├── eol_runtime │ │ └── metadata │ ├── flathub_json │ │ ├── metadata │ │ └── flathub.json │ ├── wrong-elf-arch │ │ └── metadata │ ├── appdata-quality │ │ ├── metadata │ │ ├── com.github.flathub.appdata-quality.metainfo.xml │ │ └── com.github.flathub.appdata-quality.xml │ ├── desktop-file │ │ ├── metadata │ │ ├── com.github.flathub_infra.desktop-file-Foo.desktop │ │ ├── com.github.flathub_infra.desktop-file.desktop │ │ └── com.github.flathub_infra.desktop-file.xml │ ├── finish_args_missing │ │ └── metadata │ ├── svg-screenshot │ │ ├── metadata │ │ ├── com.github.flathub.svg_screenshot.metainfo.xml │ │ └── com.github.flathub.svg_screenshot.xml │ ├── appstream-broken-icon │ │ ├── metadata │ │ ├── org.flathub.appstream_broken_icon.metainfo.xml │ │ └── org.flathub.appstream_broken_icon.xml │ ├── appstream-unsupported-ctype │ │ ├── metadata │ │ ├── org.flathub.appstream_unsupported_ctype.metainfo.xml │ │ └── org.flathub.appstream_unsupported_ctype.xml │ ├── home_host │ │ ├── home_host2 │ │ │ └── metadata │ │ ├── home_host3 │ │ │ └── metadata │ │ ├── home_host4 │ │ │ └── metadata │ │ ├── home_host5 │ │ │ └── metadata │ │ ├── home_host6 │ │ │ └── metadata │ │ ├── home_host7 │ │ │ └── metadata │ │ ├── home_host1 │ │ │ └── metadata │ │ └── home_host_false │ │ │ └── metadata │ ├── appstream-cid-mismatch-flatpak-id │ │ ├── metadata │ │ ├── org.flathub.appstream-cid-mismatch-flatpak-id.metainfo.xml │ │ └── org.flathub.appstream-cid-mismatch-flatpak-id.xml │ ├── finish_args_new_metadata │ │ └── metadata │ ├── display-supported │ │ └── metadata │ ├── min_success_metadata │ │ ├── org.flathub.cli │ │ │ ├── metadata │ │ │ ├── org.flathub.cli.metainfo.xml │ │ │ └── org.flathub.cli.xml │ │ ├── org.electronjs.Electron200.BaseApp │ │ │ └── metadata │ │ ├── org.flathub.gui │ │ │ ├── metadata │ │ │ ├── org.flathub.gui.desktop │ │ │ ├── org.flathub.gui.appdata.xml │ │ │ └── org.flathub.gui.xml │ │ └── org.gtk.Gtk33theme.Helium-dark │ │ │ └── metadata │ ├── misplaced-icons │ │ ├── metadata │ │ ├── org.flathub.example.misplaced-icons.desktop │ │ ├── org.flathub.example.misplaced-icons.appdata.xml │ │ └── org.flathub.example.misplaced-icons.xml │ ├── wrong-rdns-appid │ │ └── metadata │ ├── appstream-missing-timestamp │ │ ├── metadata │ │ ├── org.flathub.appstream_no_timestamp.appdata.xml │ │ └── org.flathub.appstream_no_timestamp.xml │ ├── dconf-access │ │ └── metadata │ ├── appstream-manifest-url-unreachable │ │ ├── metadata │ │ └── org.flathub.appstream_manifest_url_unreachable.xml │ ├── finish_args_xdg_dirs │ │ └── metadata │ ├── metadata-spaces │ │ └── metadata │ └── finish_args │ │ └── metadata ├── cli │ ├── user_exceptions.json │ └── io.github.flathub.flathub.json ├── repo │ └── min_success_metadata │ │ ├── gui-app │ │ ├── org.flathub.gui.png │ │ ├── org.flathub.gui.desktop │ │ ├── org.flathub.gui.metainfo.xml │ │ └── org.flathub.gui.yaml │ │ ├── baseapp │ │ └── org.flathub.BaseApp.yaml │ │ ├── extension │ │ ├── org.freedesktop.Sdk.Extension.flathub.metainfo.xml │ │ └── org.freedesktop.Sdk.Extension.flathub.yaml │ │ └── console-app │ │ ├── org.flathub.cli.yaml │ │ └── org.flathub.cli.metainfo.xml ├── python3-pytest.json ├── test_httpserver.py └── flatmanager.sh ├── .git-blame-ignore-revs ├── CODEOWNERS ├── docker ├── passwd ├── Dockerfile.unprivileged ├── rewrite-manifest.py ├── Dockerfile └── build.sh ├── .gitignore ├── .github └── workflows │ ├── label.yml │ ├── janitor_exceptions.yml │ └── release.yml ├── LICENSE ├── utils ├── merge.py └── create_exceptions.py ├── .pre-commit-config.yaml └── pyproject.toml /flatpak_builder_lint/staticfiles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/manifests/symlinks/symlink.json: -------------------------------------------------------------------------------- 1 | source.json -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 75a467ecd3a255340bcc7d857891aa62bbb966d4 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | /staticfiles/exceptions.json @flathub-infra/reviewers 2 | -------------------------------------------------------------------------------- /tests/manifests/appid.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "com.github.flathub" 3 | } 4 | -------------------------------------------------------------------------------- /tests/manifests/appid-too-few-cpts.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.github.foo" 3 | } 4 | -------------------------------------------------------------------------------- /tests/manifests/exceptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.exceptions" 3 | } 4 | -------------------------------------------------------------------------------- /tests/manifests/symlinks/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "com.github.flathub" 3 | } 4 | -------------------------------------------------------------------------------- /tests/manifests/unknown-properties-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "i-am-an-invalid-key": "foobar" 3 | } 4 | -------------------------------------------------------------------------------- /tests/manifests/git-repo-checks/git-repo-checks.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "com.github.flathub" 3 | } 4 | -------------------------------------------------------------------------------- /tests/manifests/unknown-properties-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "x-i-am-an-custom-valid-key": "foobar" 3 | } 4 | -------------------------------------------------------------------------------- /docker/passwd: -------------------------------------------------------------------------------- 1 | root:x:0:0:root:/root:/bin/bash 2 | flatbld:x:1001:1001:flatpak:/home/flatbld:/bin/bash 3 | -------------------------------------------------------------------------------- /tests/manifests/exceptions_wildcard.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.exceptions_wildcard" 3 | } 4 | -------------------------------------------------------------------------------- /tests/manifests/finish_args_missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.finish_args_missing" 3 | } 4 | -------------------------------------------------------------------------------- /tests/builddir/appstream-no-icon-file/org.flathub.appstream_no_icon_file.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | -------------------------------------------------------------------------------- /tests/builddir/appstream-broken-remote-icon/org.flathub.appstream_broken_remote_icon.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | -------------------------------------------------------------------------------- /tests/builddir/appstream-icon-key-no-type/org.flathub.appstream_icon_key_no_type.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | -------------------------------------------------------------------------------- /tests/manifests/git-repo-checks/.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "baz"] 2 | path = baz 3 | url = https://github.com/foobar/baz.git 4 | -------------------------------------------------------------------------------- /tests/cli/user_exceptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "io.github.flathub.flathub": [ 3 | "appid-filename-mismatch" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /flatpak_builder_lint/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib import metadata 2 | 3 | __version__ = metadata.version(__package__) 4 | del metadata 5 | -------------------------------------------------------------------------------- /tests/builddir/appid/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=com.github.flathub.desktop 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/manifests/finish_args_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.finish_args_empty", 3 | "finish-args": [ 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/builddir/console/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.example.console 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/eol_runtime/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=com.github.flathub.eol_runtime 3 | runtime=org.freedesktop.Platform/x86_64/18.08 4 | -------------------------------------------------------------------------------- /tests/builddir/flathub_json/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.flathub_json 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/wrong-elf-arch/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=com.github.flathub.desktop 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | -------------------------------------------------------------------------------- /tests/builddir/appdata-quality/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=com.github.flathub.appdata-quality 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/desktop-file/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=com.github.flathub_infra.desktop-file 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/finish_args_missing/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.finish_args_empty 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/svg-screenshot/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=com.github.flathub.svg_screenshot 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/manifests/eol_runtime.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.eol_runtime", 3 | "runtime": "org.gnome.Sdk", 4 | "runtime-version": "40" 5 | } 6 | -------------------------------------------------------------------------------- /tests/builddir/appstream-broken-icon/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream_broken_icon 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/appstream-no-icon-file/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream_no_icon_file 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/manifests/display-supported3.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.display_supported", 3 | "finish-args": [ 4 | "--socket=x11" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/builddir/appstream-icon-key-no-type/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream_icon_key_no_type 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/appstream-unsupported-ctype/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream_unsupported_ctype 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/console/org.flathub.example.console.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Type=Application 4 | Name=Console 5 | Hidden=true 6 | NoDisplay=false 7 | -------------------------------------------------------------------------------- /tests/manifests/display-only-wayland.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.display_only_wayland", 3 | "finish-args": [ 4 | "--socket=wayland" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host2.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host2", 3 | "finish-args": [ 4 | "--filesystem=~" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/builddir/appstream-broken-remote-icon/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream_broken_remote_icon 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host3.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host3", 3 | "finish-args": [ 4 | "--filesystem=~/" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host4.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host4", 3 | "finish-args": [ 4 | "--filesystem=~/:rw" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host2/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host2 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=~; 7 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host3/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host3 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=~/; 7 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host5.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host5", 3 | "finish-args": [ 4 | "--filesystem=home:rw" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host6.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host6", 3 | "finish-args": [ 4 | "--filesystem=home:ro" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host7.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host7", 3 | "finish-args": [ 4 | "--filesystem=host:ro" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/builddir/appstream-cid-mismatch-flatpak-id/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream-cid-mismatch-flatpak-id 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host4/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host4 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=~/:rw; 7 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host5/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host5 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=home:rw; 7 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host6/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host2 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=home:ro 7 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host7/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host7 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=host:ro 7 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host1/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host1 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=home;host; 7 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/gui-app/org.flathub.gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loathingKernel/flatpak-builder-lint/master/tests/repo/min_success_metadata/gui-app/org.flathub.gui.png -------------------------------------------------------------------------------- /tests/builddir/finish_args_new_metadata/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.finish_args_new_metadata 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | devices=usb; 7 | -------------------------------------------------------------------------------- /tests/manifests/finish_args-wayland-x11.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.finish_args", 3 | "finish-args": [ 4 | "--socket=wayland", 5 | "--socket=x11" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/manifests/finish_args-incorrect_secrets-talk-name.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.finish_args", 3 | "finish-args": [ 4 | "--talk-name=org.freedesktop.Secrets" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/manifests/toplevel.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.TopLevel", 3 | "branch": "stable", 4 | "default-branch": "stable", 5 | "cleanup": [ 6 | "/lib/debug" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/builddir/display-supported/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.display_supported 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | sockets=fallback-x11;wayland; 7 | 8 | -------------------------------------------------------------------------------- /tests/manifests/display-supported2.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.display_supported", 3 | "finish-args": [ 4 | "--socket=fallback-x11", 5 | "--socket=wayland" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host1.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host1", 3 | "finish-args": [ 4 | "--filesystem=home", 5 | "--filesystem=host" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/builddir/desktop-file/com.github.flathub_infra.desktop-file-Foo.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | # Missing type deliberately broken 3 | Version=1.0 4 | Name=Desktop application 5 | Comment=A desktop application 6 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.flathub.cli/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.cli 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | sdk=org.freedesktop.Sdk/x86_64/23.08 5 | command=foo 6 | 7 | -------------------------------------------------------------------------------- /tests/manifests/base_app.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.TopLevel.BaseApp", 3 | "branch": "stable", 4 | "default-branch": "stable", 5 | "cleanup": [ 6 | "/lib/debug/source" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tests/manifests/finish_args-new-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.finish_args_new_metadata", 3 | "finish-args": [ 4 | "--device=input", 5 | "--require-version=1.11.0" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/manifests/flathub.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip-appstream-check": "true", 3 | "end-of-life": "message", 4 | "end-of-life-rebase": "foo", 5 | "publish-delay-hours": 2, 6 | "only-arches": ["x86_64", "i386"] 7 | } 8 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.electronjs.Electron200.BaseApp/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.electronjs.Electron200.BaseApp 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | sdk=org.freedesktop.Sdk/x86_64/23.08 5 | -------------------------------------------------------------------------------- /tests/builddir/flathub_json/flathub.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip-appstream-check": "true", 3 | "end-of-life": "message", 4 | "end-of-life-rebase": "foo", 5 | "publish-delay-hours": 2, 6 | "only-arches": ["x86_64", "i386"] 7 | } 8 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.flathub.gui/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.gui 3 | runtime=org.gnome.Platform/x86_64/45 4 | sdk=org.gnome.Sdk/x86_64/45 5 | command=foo 6 | 7 | [Context] 8 | filesystems=xdg-download; 9 | -------------------------------------------------------------------------------- /tests/builddir/misplaced-icons/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.example.misplaced-icons 3 | runtime=org.gnome.Platform/x86_64/45 4 | sdk=org.gnome.Sdk/x86_64/45 5 | command=foo 6 | 7 | [Context] 8 | filesystems=xdg-download; 9 | -------------------------------------------------------------------------------- /tests/builddir/wrong-rdns-appid/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=io.gitlab.wwwwwwwwwwwwwwwwwwwww.bar 3 | runtime=org.gnome.Platform/x86_64/45 4 | sdk=org.gnome.Sdk/x86_64/45 5 | command=foo 6 | 7 | [Context] 8 | filesystems=xdg-download; 9 | -------------------------------------------------------------------------------- /tests/builddir/home_host/home_host_false/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.home_host2 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=home/foobar;home/foobarbaz:ro;~/foobar:ro;host-os:ro;host-etc; 7 | -------------------------------------------------------------------------------- /tests/builddir/appstream-missing-timestamp/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream_no_timestamp 3 | runtime=org.gnome.Platform/x86_64/45 4 | sdk=org.gnome.Sdk/x86_64/45 5 | command=foo 6 | 7 | [Context] 8 | filesystems=xdg-download; 9 | -------------------------------------------------------------------------------- /tests/builddir/dconf-access/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.dconf-access 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=xdg-run/dconf; 7 | 8 | [Session Bus Policy] 9 | ca.desrt.dconf=talk 10 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/gui-app/org.flathub.gui.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Example 3 | GenericName=Example 4 | Comment=Example 5 | Exec=hello %U 6 | Icon=org.flathub.gui 7 | Type=Application 8 | Categories=Network; 9 | Version=1.1 10 | -------------------------------------------------------------------------------- /docker/Dockerfile.unprivileged: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/flathub-infra/flatpak-builder-lint:latest 2 | 3 | ADD passwd /etc/passwd 4 | RUN install -d -m755 -o 1001 -g 1001 /home/flatbld 5 | 6 | USER flatbld 7 | WORKDIR /home/flatbld 8 | ENTRYPOINT ["/app/bin/flatpak-builder-lint"] 9 | -------------------------------------------------------------------------------- /tests/builddir/appstream-manifest-url-unreachable/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.appstream_manifest_url_unreachable 3 | runtime=org.gnome.Platform/x86_64/45 4 | sdk=org.gnome.Sdk/x86_64/45 5 | command=foo 6 | 7 | [Context] 8 | filesystems=xdg-download; 9 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.flathub.gui/org.flathub.gui.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Example 3 | GenericName=Example 4 | Comment=Example 5 | Exec=Example %U 6 | Icon=org.flathub.gui 7 | Type=Application 8 | Categories=Network; 9 | Version=1.1 10 | -------------------------------------------------------------------------------- /tests/builddir/misplaced-icons/org.flathub.example.misplaced-icons.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Example 3 | GenericName=Example 4 | Comment=Example 5 | Exec=foo %u 6 | Icon=org.flathub.example.misplaced-icons 7 | Type=Application 8 | Categories=Network; 9 | Version=1.1 10 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/com.github.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "com.github", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/ch.wwwwww.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "ch.wwwwww.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .mypy_cache 3 | .pytest_cache 4 | .ruff_cache 5 | nohup.out 6 | server.pid 7 | builddir/ 8 | flatpak_builder_lint.egg-info/ 9 | !tests/builddir/ 10 | build/ 11 | repo/ 12 | !tests/repo/ 13 | .flatpak-builder/ 14 | .venv/ 15 | *.gz 16 | *.svg 17 | *.png 18 | !tests/repo/min_success_metadata/gui-app/org.flathub.gui.png 19 | -------------------------------------------------------------------------------- /tests/manifests/home_host/org.flathub.home_host_false.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.home_host_false", 3 | "finish-args": [ 4 | "--filesystem=home/foobar", 5 | "--filesystem=home/foobar:ro", 6 | "--filesystem=~/foobar:ro", 7 | "--filesystem=host-os:ro", 8 | "--filesystem=host-etc" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.github.ghost.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.github.ghost.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/builddir/finish_args_xdg_dirs/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.finish_args_xdg_dirs 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | filesystems=xdg-config:ro;xdg-cache:ro;xdg-data:ro;xdg-config;xdg-cache;xdg-data;xdg-config:create;xdg-cache:create;xdg-data:create;xdg-cache/electron:create;xdg-config/fonts;xdg-data/gvfs/foo:ro; 7 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/com.--github--.flathub.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "com.--github--.flathub", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.eu.encom.spectral.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.eu.encom.spectral", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.frama.flopedt.FlOpEDT.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.frama.flopedt.FlOpEDT", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.github.flatpak.flatpak.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.github.flatpak.flatpak", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.sourceforge.xampp.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.sourceforge.xampp.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.frama.wwwwwwwwwwwww.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.frama.wwwwwwwwwwwww.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.github.wwwwwwwwwwwww.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.github.wwwwwwwwwwwww.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.gitlab.deeplomatics.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.gitlab.deeplomatics.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.gitlab.wwwwwwwwwwwww.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.gitlab.wwwwwwwwwwwww.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.gnome.design.VectorSlicer.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.gnome.design.VectorSlicer", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.github.wwwwwwwwwwwww.foo.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.github.wwwwwwwwwwwww.foo.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.gnome.gitlab.YaLTeR.Identity.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.gnome.gitlab.YaLTeR.Identity", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/page.codeberg.wwwwwwwwwwwww.foo.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "page.codeberg.wwwwwwwwwwwww.foo", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/io.sourceforge.wwwwwwwwwwwwwwww.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "io.sourceforge.wwwwwwwwwwwwwwww.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.gnome.gitlab.wwwwwwwwwwwww.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.gnome.gitlab.wwwwwwwwwwwww.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.gnome.gitlab.user.project.foo.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.gnome.gitlab.user.project.foo.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/page.codeberg.forgejo.code-of-conduct.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "page.codeberg.forgejo.code-of-conduct", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.freedesktop.gitlab.wwwwwwwwwwwww.bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.freedesktop.gitlab.wwwwwwwwwwwww.bar", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/builddir/desktop-file/com.github.flathub_infra.desktop-file.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=Desktop application 4 | Comment=A desktop application 5 | # Empty has flatpak run 6 | Exec=/usr/bin/flatpak run foo 7 | # Icon does not match app-id 8 | Icon=org.flathub.foo 9 | Terminal=false 10 | Type=Application 11 | # Intentional whitespace 12 | Hidden = true 13 | Categories=GTK;Qt 14 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.gtk.Gtk33theme.Helium-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.gtk.Gtk33theme.Helium-dark", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "build-extension": true, 7 | "finish-args": ["--foo=bar"], 8 | "modules": [ 9 | { 10 | "name": "module2", 11 | "buildsystem": "autotools" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.electronjs.Electron200.BaseApp.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.electronjs.Electron200.BaseApp", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "build-extension": true, 7 | "finish-args": ["--foo=bar"], 8 | "modules": [ 9 | { 10 | "name": "module2", 11 | "buildsystem": "autotools" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/manifests/domain_checks/org.freedesktop.gitlab.drm_hwcomposer.drm-hwcomposer.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.freedesktop.gitlab.drm_hwcomposer.drm-hwcomposer", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": ["--foo=bar"], 7 | "modules": [ 8 | { 9 | "name": "module2", 10 | "buildsystem": "autotools" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.gtk.Gtk33theme.Helium-dark/metadata: -------------------------------------------------------------------------------- 1 | [Runtime] 2 | name=org.gtk.Gtk33theme.Helium-dark 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | sdk=org.freedesktop.Sdk/x86_64/23.08 5 | 6 | [ExtensionOf] 7 | ref=app/org.foo.bar/x86_64/stable 8 | runtime=org.freedesktop.Sdk/x86_64/23.08 9 | 10 | [Session Bus Policy] 11 | org.freedesktop.ScreenSaver=talk 12 | org.freedesktop.PowerManagement=talk 13 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/baseapp/org.flathub.BaseApp.yaml: -------------------------------------------------------------------------------- 1 | app-id: org.flathub.BaseApp 2 | runtime: org.freedesktop.Platform 3 | runtime-version: '25.08' 4 | sdk: org.freedesktop.Sdk 5 | modules: 6 | - name: hello 7 | buildsystem: simple 8 | build-commands: 9 | - install -Dm755 hello.sh /app/bin/hello 10 | sources: 11 | - type: script 12 | dest-filename: hello.sh 13 | commands: 14 | - echo "Hello world, from a sandbox" 15 | -------------------------------------------------------------------------------- /tests/manifests/yaml/manfiest-valid.yml: -------------------------------------------------------------------------------- 1 | id: org.flatpak.Hello 2 | runtime: org.freedesktop.Platform 3 | runtime-version: '23.08' 4 | sdk: org.freedesktop.Sdk 5 | command: hello 6 | modules: 7 | - name: hello 8 | buildsystem: simple 9 | build-commands: 10 | - install -Dm755 hello.sh /app/bin/hello 11 | sources: 12 | - type: script 13 | dest-filename: hello.sh 14 | commands: 15 | - echo "Hello world, from a sandbox" 16 | -------------------------------------------------------------------------------- /tests/manifests/yaml/manfiest-invalid.yml: -------------------------------------------------------------------------------- 1 | id: org.flatpak.Hello 2 | runtime: org.freedesktop.Platform 3 | runtime-version: '23.08' 4 | runtime-version: '23.08' 5 | sdk: org.freedesktop.Sdk 6 | command: hello 7 | modules: 8 | - name: hello 9 | buildsystem: simple 10 | build-commands: 11 | - install -Dm755 hello.sh /app/bin/hello 12 | sources: 13 | - type: script 14 | dest-filename: hello.sh 15 | commands: 16 | - echo "Hello world, from a sandbox" 17 | -------------------------------------------------------------------------------- /docker/rewrite-manifest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import json 4 | 5 | with open("org.flatpak.Builder/org.flatpak.Builder.json") as f: 6 | manifest = json.load(f) 7 | 8 | for module in manifest["modules"]: 9 | if isinstance(module, dict) and module.get("name") == "flatpak-builder-lint": 10 | module["sources"] = [{"type": "dir", "path": "../.."}] 11 | break 12 | 13 | with open("org.flatpak.Builder/org.flatpak.Builder.json", "w") as f: 14 | json.dump(manifest, f, indent=4) 15 | -------------------------------------------------------------------------------- /tests/manifests/flathub_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.json", 3 | "modules": [ 4 | { 5 | "name": "libyaml", 6 | "sources": [ 7 | { 8 | "type": "archive", 9 | "url": "https://github.com/yaml/libyaml/releases/download/0.2.5/yaml-0.2.5.tar.gz", 10 | "sha256": "c642ae9b75fee120b2d96c712538bd2cf283228d2337df2cf2988e3c02678ef4" 11 | } 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/manifests/dconf.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.dconf-access", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": [ 7 | "--filesystem=xdg-run/dconf:ro", 8 | "--filesystem=~/.config/dconf:ro", 9 | "--filesystem=/run/user/1000/dconf/foo:ro", 10 | "--talk-name=ca.desrt.dconf", 11 | "--own-name=ca.desrt.dconf" 12 | ], 13 | "modules": [ 14 | { 15 | "name": "module2", 16 | "buildsystem": "autotools" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/extension/org.freedesktop.Sdk.Extension.flathub.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.freedesktop.Sdk.Extension.flathub 4 | org.freedesktop.Platform 5 | Example extension 6 | Example extension 7 | https://github.com/flathub 8 | CC0-1.0 9 | GPL-2.0 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/manifests/com.example.json_warnings.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "com.example.json_warnings", 3 | "runtime": "org.kde.Platform", 4 | "runtime-version": "6.9", 5 | "sdk": "org.kde.Sdk", 6 | "command": "foo", 7 | "finish-args": [ 8 | "--device=dri" 9 | ], 10 | "modules": [ 11 | { 12 | "name": "kpat", 13 | "buildsystem": "cmake-ninja", 14 | "sources": 15 | { 16 | "type": "git", 17 | "url": "https://invent.kde.org/kde/kpat.git" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/freedesktopsdk/sdk:25.08 2 | 3 | RUN echo "root:x:0:0:root:/root:/bin/bash" > /etc/passwd && \ 4 | echo "root:x:0:" > /etc/group 5 | 6 | RUN rm -rf /tmp /var/tmp /root /etc/gitconfig && mkdir -p -m 1777 /tmp /var/tmp && \ 7 | mkdir -p -m 700 /root 8 | 9 | ENV LANG=C.UTF-8 10 | ENV LC_ALL=C.UTF-8 11 | ENV PATH="/app/bin:$PATH" 12 | ENV LD_LIBRARY_PATH=/app/lib 13 | ENV PYTHONPATH=/app/lib/python3.13/site-packages 14 | ENV GI_TYPELIB_PATH=/app/lib/girepository-1.0 15 | 16 | ADD org.flatpak.Builder/builddir/files /app 17 | 18 | ENTRYPOINT ["/app/bin/flatpak-builder-lint"] 19 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/extension/org.freedesktop.Sdk.Extension.flathub.yaml: -------------------------------------------------------------------------------- 1 | id: org.freedesktop.Sdk.Extension.flathub 2 | branch: stable 3 | runtime: org.freedesktop.Sdk 4 | runtime-version: '25.08' 5 | sdk: org.freedesktop.Sdk 6 | build-extension: true 7 | separate-locales: false 8 | build-options: 9 | prefix: /app/plugins/foobar 10 | modules: 11 | - name: hello 12 | buildsystem: simple 13 | build-commands: 14 | - install -Dm644 --target-directory=${FLATPAK_DEST}/share/metainfo ${FLATPAK_ID}.metainfo.xml 15 | sources: 16 | - type: file 17 | path: org.freedesktop.Sdk.Extension.flathub.metainfo.xml 18 | -------------------------------------------------------------------------------- /tests/cli/io.github.flathub.flathub.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "io.github.flathub.flathub", 3 | "runtime": "org.kde.Platform", 4 | "runtime-version": "6.8", 5 | "sdk": "org.kde.Sdk", 6 | "command": "foo", 7 | "finish-args": [ 8 | "--device=dri", 9 | "--share=ipc", 10 | "--socket=fallback-x11", 11 | "--socket=wayland" 12 | ], 13 | "modules": [ 14 | { 15 | "name": "test", 16 | "sources": [ 17 | { 18 | "type": "file", 19 | "path": "test.desktop" 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/manifests/finish_args_fs_negative.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.finish_args_fs_negative", 3 | "finish-args": [ 4 | "--filesystem=~/.config/foobar/baz:create", 5 | "--filesystem=~/.config/foobar/baz", 6 | "--filesystem=~/.local/foobar/baz/", 7 | "--filesystem=~/.local/share/baz", 8 | "--filesystem=~/.cache/share/baz/", 9 | "--filesystem=~/.autostart/baz", 10 | "--filesystem=~/.local/share/applications/baz", 11 | "--filesystem=home/.autostart/baz", 12 | "--filesystem=home/.cache/share/baz/:ro", 13 | "--filesystem=home/.cache/share/baz:create", 14 | "--filesystem=home/.cache/share/baz:ro" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/console-app/org.flathub.cli.yaml: -------------------------------------------------------------------------------- 1 | app-id: org.flathub.cli 2 | runtime: org.freedesktop.Platform 3 | runtime-version: '25.08' 4 | sdk: org.freedesktop.Sdk 5 | command: hello 6 | finish-args: 7 | - --filesystem=xdg-download 8 | - --share=network 9 | 10 | modules: 11 | - name: metainfo 12 | buildsystem: simple 13 | build-commands: 14 | - install -Dm0644 ${FLATPAK_ID}.metainfo.xml ${FLATPAK_DEST}/share/metainfo/${FLATPAK_ID}.metainfo.xml 15 | - install -Dm755 hello.sh /app/bin/hello 16 | sources: 17 | - type: file 18 | path: org.flathub.cli.metainfo.xml 19 | - type: script 20 | dest-filename: hello.sh 21 | commands: 22 | - echo "Hello world, from a sandbox" 23 | -------------------------------------------------------------------------------- /tests/builddir/console/org.flathub.example.console.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.example.console 4 | Example Console 5 | Example Console Application 6 | https://example.org 7 | Flathub 8 | 9 |

This is an example

10 |
11 | 12 | foo 13 | 14 | 15 | 16 | 17 | CC0-1.0 18 | GPL-2.0 19 | 20 |
21 | -------------------------------------------------------------------------------- /tests/builddir/appstream-unsupported-ctype/org.flathub.appstream_unsupported_ctype.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.example.console 4 | Example Console 5 | Example Console Application 6 | https://example.org 7 | Flathub 8 | 9 |

This is an example

10 |
11 | 12 | foo 13 | 14 | 15 | 16 | 17 | CC0-1.0 18 | GPL-2.0 19 | 20 |
21 | -------------------------------------------------------------------------------- /tests/manifests/module-nightly-x-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.module-nightly-x-checker", 3 | "modules": [ 4 | { 5 | "name": "module1", 6 | "buildsystem": "cmake", 7 | "config-opts": [ 8 | "-DCMAKE_BUILD_TYPE=Debug" 9 | ], 10 | "sources": [ 11 | { 12 | "type": "archive", 13 | "url": "https://example.com/foo.tar.gz", 14 | "sha1": "deadbeef", 15 | "x-checker-data": { 16 | "type": "json", 17 | "url": "https://example.com/releases/latest", 18 | "commit-query": ".sha" 19 | } 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/manifests/xdg-dirs-access.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "xdg-dirs-access", 3 | "runtime": "foo", 4 | "sdk": "bar", 5 | "command": "foo", 6 | "finish-args": [ 7 | "--filesystem=xdg-config:ro", 8 | "--filesystem=xdg-cache:ro", 9 | "--filesystem=xdg-data:ro", 10 | "--filesystem=xdg-config", 11 | "--filesystem=xdg-cache", 12 | "--filesystem=xdg-data", 13 | "--filesystem=xdg-config:create", 14 | "--filesystem=xdg-cache:create", 15 | "--filesystem=xdg-data:create", 16 | "--filesystem=xdg-cache/electron:create", 17 | "--filesystem=xdg-config/fonts", 18 | "--filesystem=xdg-data/gvfs/foo:ro" 19 | ], 20 | "modules": [ 21 | { 22 | "name": "module2", 23 | "buildsystem": "autotools" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/builddir/console/org.flathub.example.console.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.example.console 5 | Example Console 6 | Example Console Application 7 | https://example.org 8 | Flathub 9 | 10 |

This is an example

11 |
12 | 13 | foo 14 | 15 | 16 | 17 | 18 | CC0-1.0 19 | GPL-2.0 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /tests/builddir/appstream-unsupported-ctype/org.flathub.appstream_unsupported_ctype.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream_unsupported_ctype 5 | Example Console 6 | Example Console Application 7 | https://example.org 8 | Flathub 9 | 10 |

This is an example

11 |
12 | 13 | foo 14 | 15 | 16 | 17 | 18 | CC0-1.0 19 | GPL-2.0 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from .. import ostree 4 | 5 | ALL = [] 6 | 7 | 8 | class CheckMeta(type): 9 | def __init__(cls, *args, **kwargs) -> None: # type: ignore 10 | super().__init__(*args, **kwargs) 11 | if object in cls.__bases__: 12 | # Don't register base class 13 | return 14 | ALL.append(cls) 15 | 16 | 17 | class Check(metaclass=CheckMeta): 18 | warnings: ClassVar[set[str]] = set() 19 | errors: ClassVar[set[str]] = set() 20 | jsonschema: ClassVar[set[str]] = set() 21 | appstream: ClassVar[set[str]] = set() 22 | desktopfile: ClassVar[set[str]] = set() 23 | info: ClassVar[set[str]] = set() 24 | repo_primary_refs: ClassVar[set[str]] = set() 25 | 26 | def _populate_refs(self, repo: str) -> None: 27 | if not Check.repo_primary_refs: 28 | Check.repo_primary_refs.update(ostree.get_primary_refs(repo)) 29 | -------------------------------------------------------------------------------- /tests/manifests/modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.modules", 3 | "add-extensions": { 4 | "org.flathub.modules.BundledExtension": { 5 | "directory": "foo/bar", 6 | "bundle": true 7 | }, 8 | "org.flathub.example.BundledExtension": { 9 | "directory": "foo/bar", 10 | "bundle": true 11 | } 12 | }, 13 | "modules": [ 14 | { 15 | "name": "module1", 16 | "buildsystem": "cmake", 17 | "config-opts": [ 18 | "-DCMAKE_BUILD_TYPE=Debug" 19 | ], 20 | "cleanup": [ 21 | "/lib/debug" 22 | ], 23 | "sources": [ 24 | { 25 | "type": "archive", 26 | "url": "https://example.com/foo.tar.gz", 27 | "sha1": "deadbeef" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/jsonschema.py: -------------------------------------------------------------------------------- 1 | import importlib.resources 2 | import json 3 | from collections.abc import Mapping 4 | from typing import Any 5 | 6 | import jsonschema 7 | import jsonschema.exceptions 8 | 9 | from .. import staticfiles 10 | from . import Check 11 | 12 | 13 | class JSONSchemaCheck(Check): 14 | def check_manifest(self, manifest: Mapping[str, Any]) -> None: 15 | with ( 16 | importlib.resources.files(staticfiles) 17 | .joinpath("flatpak-manifest.schema.json") 18 | .open() as f 19 | ): 20 | schema = json.load(f) 21 | 22 | try: 23 | jsonschema.validate(dict(manifest), schema) 24 | except jsonschema.exceptions.SchemaError: 25 | self.errors.add("jsonschema-schema-error") 26 | except jsonschema.exceptions.ValidationError as exc: 27 | self.errors.add("jsonschema-validation-error") 28 | self.jsonschema.add(exc.message) 29 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | name: Label pull requests 2 | 3 | # WARNING: Do NOT use org level secrets or checkout any code in this 4 | # workflow 5 | 6 | on: 7 | pull_request_target: 8 | types: [opened, edited, reopened, synchronize, ready_for_review] 9 | branches: 10 | - master 11 | paths: 12 | - 'flatpak_builder_lint/staticfiles/exceptions.json' 13 | workflow_dispatch: 14 | 15 | jobs: 16 | label: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | steps: 21 | - name: Add exceptions label 22 | run: | 23 | if ! gh pr view --json labels -q '.labels[].name' "$PR_NUM"|grep -qE "^exceptions$"; then 24 | gh pr edit "$PR_NUM" --add-label "exceptions" 25 | fi 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | GH_REPO: ${{ github.repository }} 29 | PR_NUM: ${{ github.event.pull_request.number || github.event.issue.number }} 30 | -------------------------------------------------------------------------------- /tests/builddir/metadata-spaces/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.example-spaces 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | command=example spaces 5 | 6 | [Context] 7 | persistent=.foo bar 8 | filesystems=~/foo bar 9 | 10 | [Extra Data] 11 | name=example spaces.ext 12 | uri=https://example.org/foo%20bar.ext 13 | name1=example%20spaces.ext 14 | size1=1 15 | uri1=https://example.org/foo bar.ext 16 | 17 | [Environment] 18 | ALSA_CONFIG_PATH=/usr/share/alsa/alsa-flatpak.conf 19 | GI_TYPELIB_PATH=/app/lib/girepository-1.0 20 | 21 | [Environment] 22 | GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0:/usr/lib/extensions/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0 23 | XDG_DATA_DIRS=/app/share:/usr/share:/usr/share/runtime/share:/run/host/user-share:/run/host/share 24 | ALSA_CONFIG_DIR=/usr/share/alsa 25 | __EGL_EXTERNAL_PLATFORM_CONFIG_DIRS=/etc/egl/egl_external_platform.d:/usr/lib/x86_64-linux-gnu/GL/egl/egl_external_platform.d:/usr/share/egl/egl_external_platform.d 26 | PYTHONUSERBASE=/var/data/python 27 | -------------------------------------------------------------------------------- /tests/manifests/network_access.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.build_network_access", 3 | "runtime": "org.gnome.Platform", 4 | "runtime-version": "48", 5 | "sdk": "org.gnome.Sdk", 6 | "sdk-extensions": [ 7 | "org.freedesktop.Sdk.Extension.rust-stable" 8 | ], 9 | "command": "build_network_access", 10 | "finish-args": [ 11 | "--socket=x11", 12 | "--filesystem=home" 13 | ], 14 | "build-options": { 15 | "build-args": [ 16 | "--share=network" 17 | ] 18 | }, 19 | "modules": [ 20 | { 21 | "name": "abracadabra", 22 | "buildsystem": "simple", 23 | "build-options": { 24 | "append-path": "/usr/lib/sdk/rust-stable/bin", 25 | "build-args": [ 26 | "--share=network" 27 | ] 28 | }, 29 | "build-commands": [ 30 | "cargo build --all --release", 31 | "install -D target/release/build_network_access /app/bin/build_network_access" 32 | ], 33 | "sources": [ 34 | { 35 | "type": "git", 36 | "url": "https://github.com/flathub/flathub" 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/console-app/org.flathub.cli.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.cli 4 | Example CLI 5 | Example cli application for tests 6 | FSFAP 7 | LGPL-2.1+ 8 | 9 |

10 | This is an example console-application to perform checks in the linter so that they are always successful. This is for the linter only. 11 |

12 |
13 | Flathub 14 | https://example.org 15 | 16 | foo 17 | 18 | 19 | 20 | 21 |

This release adds the following features:

22 |
    23 |
  • New tests
  • 24 |
25 |
26 |
27 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/gui-app/org.flathub.gui.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.gui 4 | 5 | org.flathub.example.gui.desktop 6 | 7 | org.flathub.gui.desktop 8 | CC0-1.0 9 | GPL-2.0+ 10 | Foo Bar 11 | Flathub 12 | Foo foo foo foo 13 | https://example.org 14 | 15 |

An example desktop application

16 |
17 | 18 | 19 | Tux 20 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 21 | 22 | 23 | https://example.org 24 | 25 | 26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /tests/builddir/appdata-quality/com.github.flathub.appdata-quality.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.flathub.appdata-quality 5 | Example 6 | A summary 7 | MIT 8 | MIT 9 | Flathub 10 | https://example.org/ 11 | com.github.flathub.appdata-quality.desktop 12 | 13 |

Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo

14 |
15 | 16 | 17 | https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Tux.svg/265px-Tux.svg.png 18 | Foo Foo Foo Foo 19 | 20 | 21 | 22 | Foo 23 | 24 | 25 | Utility 26 | 27 |
28 | -------------------------------------------------------------------------------- /tests/repo/min_success_metadata/gui-app/org.flathub.gui.yaml: -------------------------------------------------------------------------------- 1 | app-id: org.flathub.gui 2 | runtime: org.freedesktop.Platform 3 | runtime-version: '25.08' 4 | sdk: org.freedesktop.Sdk 5 | command: hello 6 | finish-args: 7 | - --filesystem=xdg-download 8 | - --share=network 9 | - --socket=fallback-x11 10 | - --socket=wayland 11 | - --share=ipc 12 | 13 | modules: 14 | - name: metainfo 15 | buildsystem: simple 16 | build-commands: 17 | - install -Dm0644 ${FLATPAK_ID}.metainfo.xml ${FLATPAK_DEST}/share/metainfo/${FLATPAK_ID}.metainfo.xml 18 | - install -Dm0644 ${FLATPAK_ID}.png ${FLATPAK_DEST}/share/icons/hicolor/128x128/apps/${FLATPAK_ID}.png 19 | - install -Dm0644 ${FLATPAK_ID}.desktop ${FLATPAK_DEST}/share/applications/${FLATPAK_ID}.desktop 20 | - install -Dm755 hello.sh /app/bin/hello 21 | sources: 22 | - type: file 23 | path: org.flathub.gui.metainfo.xml 24 | - type: file 25 | path: org.flathub.gui.png 26 | - type: file 27 | path: org.flathub.gui.desktop 28 | - type: script 29 | dest-filename: hello.sh 30 | commands: 31 | - echo "Hello world, from a sandbox" 32 | -------------------------------------------------------------------------------- /tests/builddir/svg-screenshot/com.github.flathub.svg_screenshot.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.flathub.svg_screenshot 5 | Example 6 | A summary 7 | MIT 8 | MIT 9 | Flathub 10 | https://example.org/ 11 | com.github.flathub.svg_screenshot.desktop 12 | 13 |

Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo

14 |
15 | 16 | 17 | https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Tux.svg/265px-Tux.svg 18 | Foo Foo Foo Foo 19 | 20 | 21 | 22 | Foo 23 | 24 | 25 | Utility 26 | 27 |
28 | -------------------------------------------------------------------------------- /tests/builddir/finish_args/metadata: -------------------------------------------------------------------------------- 1 | [Application] 2 | name=org.flathub.finish_args 3 | runtime=org.freedesktop.Platform/x86_64/23.08 4 | 5 | [Context] 6 | share=!network; 7 | sockets=x11;wayland;session-bus;fallback-x11;!cups;gpg-agent;ssh-auth; 8 | devices=all;!dri; 9 | filesystems=home;xdg-config/kdeglobals:ro;host;/run/media/foo;~/.var/app;/tmp/foo;~/.local/share/flatpak;/var;home/.icons;~/.themes;home/.fonts/;~/.gnupg;~/.ssh;~/.config/autostart;~/.local/share/applications;~/.config/systemd;~/.cache;~/.local;host-root; 10 | 11 | [Session Bus Policy] 12 | org.freedesktop.portal.*=talk 13 | org.freedesktop.*=talk 14 | org.gnome.*=own 15 | org.kde.*=own 16 | org.freedesktop.Flatpak=talk 17 | org.flathub.finish_args=own 18 | org.kde.StatusNotifierItem=own 19 | org.gtk.vfs=talk 20 | org.freedesktop.DBus.foo=talk 21 | org.mpris.MediaPlayer2.org.flathub.finish_args=own 22 | org.freedesktop.impl.portal.PermissionStore=own 23 | org.foo.bar=none 24 | org.freedesktop.systemd1=talk 25 | org.freedesktop.login1=own 26 | org.kde.KWin=own 27 | org.kde.plasmashell=talk 28 | 29 | [System Bus Policy] 30 | org.gnome.*=own 31 | org.freedesktop.DBus.foo=talk 32 | -------------------------------------------------------------------------------- /tests/builddir/misplaced-icons/org.flathub.example.misplaced-icons.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.example.misplaced-icons 4 | 5 | org.flathub.example.misplaced-icons.desktop 6 | 7 | org.flathub.example.misplaced-icons.desktop 8 | CC0-1.0 9 | GPL-2.0+ 10 | Foo Bar 11 | Flathub 12 | Foo foo foo foo 13 | https://example.org 14 | 15 |

An example desktop application

16 |
17 | 18 | 19 | Tux 20 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 21 | 22 | 23 | https://example.org 24 | 25 | 26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bartłomiej Piotrowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/builddir/appstream-missing-timestamp/org.flathub.appstream_no_timestamp.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.appstream_no_timestamp 4 | 5 | org.flathub.appstream_no_timestamp.desktop 6 | 7 | org.flathub.appstream_no_timestamp.desktop 8 | CC0-1.0 9 | GPL-2.0+ 10 | Foo Bar 11 | Flathub 12 | Foo foo foo foo 13 | https://example.org 14 | 15 |

An example desktop application

16 |
17 | 18 | 19 | Tux 20 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 21 | foofoo 22 | 23 | 24 | https://example.org 25 | 26 | 27 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.flathub.gui/org.flathub.gui.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.gui 4 | 5 | org.flathub.example.gui.desktop 6 | 7 | org.flathub.gui.desktop 8 | CC0-1.0 9 | GPL-2.0+ 10 | Foo Bar 11 | Flathub 12 | Foo foo foo foo 13 | https://example.org 14 | 15 |

An example desktop application

16 |

A random link in description allowed by Flathub Appstream https://example.com/

17 |
18 | 19 | 20 | Tux 21 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 22 | 23 | 24 | https://example.org 25 | 26 | 27 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | TOP_DIR="$(pwd)" 6 | 7 | checkcmd() { 8 | if ! command -v "$1" > /dev/null 2>&1; then 9 | echo "$1 not found. Please install it from your distribution." 10 | exit 1 11 | fi 12 | } 13 | 14 | checkcmd "python3" 15 | checkcmd "git" 16 | checkcmd "flatpak" 17 | checkcmd "flatpak-builder" 18 | checkcmd "docker" 19 | 20 | flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo 21 | flatpak install --or-update --user -y flathub org.flatpak.Builder 22 | 23 | cd "$TOP_DIR"/docker 24 | rm -rf org.flatpak.Builder 25 | git clone --depth=1 --branch master --recursive --single-branch https://github.com/flathub/org.flatpak.Builder.git 26 | cp -vf flatpak-builder-lint-deps.json org.flatpak.Builder/ 27 | python3 rewrite-manifest.py 28 | cd org.flatpak.Builder 29 | 30 | flatpak run org.flatpak.Builder --user --force-clean --ccache --state-dir="$TOP_DIR/.flatpak-builder" --install-deps-from=flathub builddir org.flatpak.Builder.json 31 | rm -rf "builddir/files/lib/debug" 32 | 33 | cd "$TOP_DIR"/docker 34 | docker build -t linter:dev -f Dockerfile . 35 | rm -rf org.flatpak.Builder 36 | 37 | cd "$TOP_DIR" 38 | docker run --pull never -it --rm --entrypoint= -v "$(pwd)":/mnt:Z -w /mnt linter:dev bash 39 | -------------------------------------------------------------------------------- /tests/manifests/modules_git_disallowed.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.modules2", 3 | "modules": [ 4 | { 5 | "name": "module1", 6 | "sources": [ 7 | { 8 | "type": "git" 9 | } 10 | ] 11 | }, 12 | { 13 | "name": "module2", 14 | "sources": [ 15 | { 16 | "type": "git", 17 | "path": "../" 18 | } 19 | ] 20 | }, 21 | { 22 | "name": "module3", 23 | "sources": [ 24 | { 25 | "type": "git", 26 | "url": "ssh://foobar2.git", 27 | "tag": "foo" 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "module4", 33 | "sources": [ 34 | { 35 | "type": "git", 36 | "url": "https://example.org/foobar2.git" 37 | } 38 | ] 39 | }, 40 | { 41 | "name": "module5", 42 | "sources": [ 43 | { 44 | "type": "git", 45 | "url": "https://example.org/foobar2.git", 46 | "branch": "foobarfoo" 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.flathub.cli/org.flathub.cli.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.cli 4 | Example CLI 5 | Example cli application for tests 6 | مثال سطر الأوامر 7 | Flathub 8 | FSFAP 9 | LGPL-2.1+ 10 | 11 |

12 | This is an example console-application to perform checks in the linter so that they are always successful. This is for the linter only. 13 |

14 |

Il s'agit d'un exemple d'application console pour effectuer des vérifications dans le linter afin qu'elles réussissent toujours. C'est pour le linter seulement.

15 |
16 | 17 | Flathub 18 | 19 | https://example.org 20 | 21 | foo 22 | 23 | 24 | 25 | 26 |

This release adds the following features:

27 |
    28 |
  • New tests
  • 29 |
30 |
31 |
32 |
33 | 34 |
35 | -------------------------------------------------------------------------------- /tests/manifests/modules_git_allowed.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.modules2", 3 | "modules": [ 4 | { 5 | "name": "module1", 6 | "sources": [ 7 | { 8 | "type": "git", 9 | "url": "https://foobar2.git", 10 | "commit": "1234567890abcdef1234567890abcdef12345678" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "module2", 16 | "sources": [ 17 | { 18 | "type": "git", 19 | "url": "https://foobar2.git", 20 | "branch": "f49cf6381e322b147053b74e4500af8533ac1e4c" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "module3", 26 | "sources": [ 27 | { 28 | "type": "git", 29 | "url": "https://foobar2.git", 30 | "tag": "1234567890abcdef1234567890abcdef12345678" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "module4", 36 | "sources": [ 37 | { 38 | "type": "git", 39 | "url": "https://foobar2.git", 40 | "tag": "v1.0.0", 41 | "commit": "1234567890abcdef1234567890abcdef12345678" 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /tests/builddir/appstream-cid-mismatch-flatpak-id/org.flathub.appstream-cid-mismatch-flatpak-id.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.appstream-cid-mismatch-flatpak-id.desktop 4 | Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo 5 | Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar. 6 | https://example.org 7 | 8 |

This is an example

9 |
10 | CC0-1.0 11 | org.flathub.appstream-cid-mismatch-flatpak-id.desktop 12 | com.github.flathub.appdata-quality.png 13 | 14 | Network 15 | 16 | 17 | com.github.flathub.appdata-quality.desktop 18 | 19 | 20 | 21 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 22 | 23 | 24 | https://dl.flathub.org/assets/badges/flathub-badge-en.png 25 | 26 | 27 | Foo Foo Foo 28 | https://dl.flathub.org/assets/badges/flathub-badge-i-en.png 29 | 30 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.flathub.cli/org.flathub.cli.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.cli 5 | مثال سطر الأوامر 6 | Example CLI 7 | Example cli application for tests 8 | LGPL-2.1+ 9 | Flathub 10 | 11 |

12 | This is an example console-application to perform checks in the linter so that they are always successful. This is for the linter only. 13 |

14 |
15 | 16 |

Il s'agit d'un exemple d'application console pour effectuer des vérifications dans le linter afin qu'elles réussissent toujours. C'est pour le linter seulement.

17 |
18 | https://example.org 19 | 20 | foo 21 | 22 | 23 | en_US 24 | 25 | 26 | 27 | 28 |

This release adds the following features:

29 |
    30 |
  • New tests
  • 31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /utils/merge.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | from typing import Any 4 | 5 | 6 | def merge_duplicates(pairs: list[tuple[str, dict[str, str]]]) -> dict[str, dict[str, str]]: 7 | d: dict[str, dict[str, str]] = {} 8 | for key, val in pairs: 9 | if key in d: 10 | if isinstance(d[key], dict): 11 | d[key].update(val) 12 | else: 13 | d[key] = val 14 | return d 15 | 16 | 17 | def save_file(filename: str, data: dict[str, Any]) -> bool: 18 | try: 19 | with open(filename, "w") as f: 20 | json.dump(data, f, indent=4) 21 | f.write("\n") 22 | return True 23 | except ValueError: 24 | return False 25 | 26 | 27 | def main(argv: list[str] | None = None) -> int: 28 | parser = argparse.ArgumentParser() 29 | parser.add_argument("filenames", nargs="*", help="Input filenames") 30 | args = parser.parse_args(argv) 31 | 32 | if not args.filenames: 33 | args.filenames = ["flatpak_builder_lint/staticfiles/exceptions.json"] 34 | 35 | exit_code = 0 36 | for filename in args.filenames: 37 | with open(filename) as f: 38 | merge_data = json.load(f, object_pairs_hook=lambda pairs: merge_duplicates(pairs)) 39 | if merge_data: 40 | if not save_file(filename, merge_data): 41 | exit_code = 1 42 | else: 43 | exit_code = 1 44 | 45 | return exit_code 46 | 47 | 48 | if __name__ == "__main__": 49 | raise SystemExit(main()) 50 | -------------------------------------------------------------------------------- /tests/builddir/svg-screenshot/com.github.flathub.svg_screenshot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.flathub.svg_screenshot 5 | Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo 6 | Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar. 7 | https://example.org 8 | 9 |

This is an example

10 |
11 | CC0-1.0 12 | com.github.flathub.svg_screenshot.desktop 13 | com.github.flathub.svg_screenshot.png 14 | 15 | Network 16 | 17 | 18 | com.github.flathub.svg_screenshot.desktop 19 | 20 | 21 | 22 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 23 | 24 | 25 | https://dl.flathub.org/assets/badges/flathub-badge-en.png 26 | 27 | 28 | Foo Foo Foo 29 | https://dl.flathub.org/assets/badges/flathub-badge-i-en.png 30 | 31 | 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /tests/builddir/appdata-quality/com.github.flathub.appdata-quality.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.flathub.appdata-quality 5 | Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo 6 | Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar. 7 | https://example.org 8 | 9 |

This is an example

10 |
11 | CC0-1.0 12 | com.github.flathub.appdata-quality.desktop 13 | com.github.flathub.appdata-quality.png 14 | 15 | Network 16 | 17 | 18 | com.github.flathub.appdata-quality.desktop 19 | 20 | 21 | 22 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 23 | 24 | 25 | https://dl.flathub.org/assets/badges/flathub-badge-en.png 26 | 27 | 28 | Foo Foo Foo 29 | https://dl.flathub.org/assets/badges/flathub-badge-i-en.png 30 | 31 | 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /tests/builddir/desktop-file/com.github.flathub_infra.desktop-file.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.flathub_infra.desktop-file 5 | Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo 6 | Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar 7 | https://example.org 8 | 9 |

This is an example

10 |
11 | CC0-1.0 12 | com.github.flathub_infra.desktop-file.desktop 13 | com.github.flathub_infra.desktop-file.png 14 | 15 | Network 16 | 17 | 18 | com.github.flathub_infra.desktop-file.desktop 19 | 20 | 21 | 22 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 23 | 24 | 25 | https://dl.flathub.org/assets/badges/flathub-badge-en.png 26 | 27 | 28 | Foo Foo Foo 29 | https://dl.flathub.org/assets/badges/flathub-badge-i-en.png 30 | 31 | 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /tests/builddir/appstream-cid-mismatch-flatpak-id/org.flathub.appstream-cid-mismatch-flatpak-id.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream-cid-mismatch-flatpak-id.desktop 5 | Foo Foo Foo Foo Foo Foo Foo Foo Foo Foo 6 | Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar. 7 | https://example.org 8 | 9 |

This is an example

10 |
11 | CC0-1.0 12 | org.flathub.appstream-cid-mismatch-flatpak-id.desktop 13 | com.github.flathub.appdata-quality.png 14 | 15 | Network 16 | 17 | 18 | com.github.flathub.appdata-quality.desktop 19 | 20 | 21 | 22 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 23 | 24 | 25 | https://dl.flathub.org/assets/badges/flathub-badge-en.png 26 | 27 | 28 | Foo Foo Foo 29 | https://dl.flathub.org/assets/badges/flathub-badge-i-en.png 30 | 31 | 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/eolruntime.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from collections.abc import Mapping 3 | from typing import Any 4 | 5 | from .. import builddir, domainutils, ostree 6 | from . import Check 7 | 8 | 9 | class EolRuntimeCheck(Check): 10 | def _validate(self, runtime_ref: str) -> None: 11 | splits = runtime_ref.split("/") 12 | decomp_ref = f"{splits[0]}//{splits[2]}" 13 | eols_runtimes = domainutils.get_eol_runtimes_on_flathub() 14 | 15 | if decomp_ref in eols_runtimes: 16 | self.warnings.add(f"runtime-is-eol-{splits[0]}-{splits[2]}") 17 | 18 | def check_manifest(self, manifest: Mapping[str, Any]) -> None: 19 | runtime_id = manifest.get("runtime") 20 | runtime_br = manifest.get("runtime-version") 21 | 22 | if runtime_id is None or runtime_br is None: 23 | return 24 | 25 | runtime_ref = f"{runtime_id}/x86_64/{runtime_br}" 26 | self._validate(runtime_ref) 27 | 28 | def check_build(self, path: str) -> None: 29 | runtime_ref = builddir.get_runtime(path) 30 | if not runtime_ref: 31 | return 32 | 33 | self._validate(runtime_ref) 34 | 35 | def check_repo(self, path: str) -> None: 36 | self._populate_refs(path) 37 | refs = self.repo_primary_refs 38 | if not refs: 39 | return 40 | 41 | for ref in refs: 42 | with tempfile.TemporaryDirectory() as tmpdir: 43 | ostree.extract_subpath(path, ref, "/metadata", tmpdir) 44 | runtime_ref = builddir.get_runtime(tmpdir) 45 | if not runtime_ref: 46 | return 47 | 48 | self._validate(runtime_ref) 49 | -------------------------------------------------------------------------------- /.github/workflows/janitor_exceptions.yml: -------------------------------------------------------------------------------- 1 | name: Janitor stale exceptions 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 1,5' 7 | 8 | jobs: 9 | janitor: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | timeout-minutes: 20 15 | steps: 16 | # 4.2.2 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Purge exceptions 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: python utils/validator.py --purge-from-issue 750 25 | 26 | - name: Validate 27 | run: python utils/validator.py 28 | 29 | - name: Check for changes 30 | id: diff 31 | run: | 32 | FILE="flatpak_builder_lint/staticfiles/exceptions.json" 33 | if git diff --quiet -- "$FILE"; then 34 | echo "changed=false" >> $GITHUB_OUTPUT 35 | else 36 | echo "changed=true" >> $GITHUB_OUTPUT 37 | fi 38 | 39 | - name: Create pull request 40 | if: ${{ success() && steps.diff.outputs.changed == 'true' }} 41 | # 7.0.8 42 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e 43 | with: 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | branch-suffix: "random" 46 | author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" 47 | commit-message: "(Automated) Purge stale exceptions" 48 | title: "(Automated) Purge stale exceptions" 49 | body: "(Automated) Purge stale exceptions" 50 | delete-branch: true 51 | sign-commits: true 52 | draft: always-true 53 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/reposize.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from .. import config 5 | from . import Check 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class RepoSizeCheck(Check): 11 | @staticmethod 12 | def get_dir_size(path: str) -> int: 13 | size = 0 14 | for dirpath, _, filenames in os.walk( 15 | path, 16 | onerror=lambda e: logger.debug( 17 | "os.walk error for %s: %s: %s", path, type(e).__name__, e 18 | ), 19 | ): 20 | for f in filenames: 21 | fp = os.path.join(dirpath, f) 22 | try: 23 | if not os.path.islink(fp): 24 | size += os.path.getsize(fp) 25 | except (FileNotFoundError, PermissionError) as e: 26 | logger.debug("Failed to get size of %s: %s: %s", fp, type(e).__name__, e) 27 | continue 28 | logger.debug("Directory size for %s: %s bytes", path, size) 29 | return size 30 | 31 | def _validate(self, path: str, primary_ref_count: int = 1) -> None: 32 | BASE = 12 * 1024 * 1024 * 1024 33 | MAX = BASE * primary_ref_count if primary_ref_count > 1 else BASE 34 | 35 | repo_size = self.get_dir_size(path) 36 | 37 | if config.is_flathub_pipeline() and repo_size >= MAX: 38 | size_gb = repo_size / (1024**3) 39 | max_gb = MAX / (1024**3) 40 | self.errors.add("flatpak-repo-too-large") 41 | self.info.add( 42 | f"flatpak-repo-too-large: Flatpak repo size is {size_gb:.2f} GB" 43 | + f" exceeds limit of {max_gb:.2f} GB" 44 | ) 45 | 46 | def check_repo(self, path: str) -> None: 47 | self._populate_refs(path) 48 | primary_ref_count = len(self.repo_primary_refs) 49 | self._validate(path, primary_ref_count) 50 | -------------------------------------------------------------------------------- /utils/create_exceptions.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | 5 | 6 | def read_appid(id_input: str) -> set[str]: 7 | app_ids = set() 8 | if os.path.exists(id_input) and os.path.isfile(id_input): 9 | try: 10 | with open(id_input) as f: 11 | for line in f: 12 | app_ids.add(line.strip()) 13 | except FileNotFoundError: 14 | pass 15 | else: 16 | app_ids.add(id_input) 17 | return app_ids 18 | 19 | 20 | def generate_exceptions( 21 | app_ids: set[str], exceptions: set[str], reason: str 22 | ) -> dict[str, dict[str, str]]: 23 | reason = reason if reason else "Predates the linter rule" 24 | return {app: {ex: reason for ex in exceptions} for app in app_ids} 25 | 26 | 27 | def main(appid: str, exceptions: set[str], reason: str) -> None: 28 | reason = reason if reason else "Predates the linter rule" 29 | 30 | app_ids = read_appid(appid) 31 | 32 | if not exceptions: 33 | raise ValueError("No exceptions provided") 34 | 35 | data = generate_exceptions(app_ids, exceptions, reason) 36 | print(json.dumps(data, sort_keys=True, indent=4)) # noqa: T201 37 | 38 | 39 | if __name__ == "__main__": 40 | parser = argparse.ArgumentParser() 41 | parser.add_argument( 42 | "--appid", 43 | type=str, 44 | required=True, 45 | help="Input filename with 1 appid per line or a single appid", 46 | ) 47 | parser.add_argument( 48 | "--exception", 49 | action="extend", 50 | nargs="*", 51 | type=str, 52 | required=True, 53 | help="Input error code to create exception. Can be used multiple times", 54 | ) 55 | parser.add_argument( 56 | "--reason", 57 | type=str, 58 | help="Input optional reason string", 59 | ) 60 | args = parser.parse_args() 61 | main(args.appid, set(args.exception), args.reason) 62 | -------------------------------------------------------------------------------- /tests/builddir/min_success_metadata/org.flathub.gui/org.flathub.gui.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.gui 5 | Foo Bar 6 | Foo foo foo foo 7 | Flathub 8 |

>An example desktop application

9 | org.flathub.gui.png 10 | org.flathub.gui.png 11 | 12 | Network 13 | 14 | GPL-2.0+ 15 | https://example.org 16 | 17 | 18 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 19 | https://dl.flathub.org/media/org.flathub.example.gui-master/624x351/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 20 | https://dl.flathub.org/media/org.flathub.example.gui-master/112x63/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 21 | https://dl.flathub.org/media/org.flathub.example.gui-master/224x126/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 22 | https://dl.flathub.org/media/org.flathub.example.gui-master/752x423/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.flathub.gui.desktop 30 |
31 |
32 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: 2 | - pre-commit 3 | - pre-push 4 | default_stages: 5 | - pre-commit 6 | fail_fast: true 7 | repos: 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b 10 | hooks: 11 | - id: trailing-whitespace 12 | - id: end-of-file-fixer 13 | - id: check-added-large-files 14 | args: ['--maxkb=100'] 15 | - id: check-shebang-scripts-are-executable 16 | - id: check-executables-have-shebangs 17 | - id: check-symlinks 18 | - id: mixed-line-ending 19 | args: [--fix=lf] 20 | 21 | - repo: local 22 | hooks: 23 | - id: uv-lock 24 | name: uv lock 25 | description: Sync uv lock 26 | entry: uv lock --quiet 27 | language: python 28 | pass_filenames: false 29 | - id: ruff-format 30 | name: ruff format 31 | description: Format with ruff 32 | entry: uv run --frozen -q ruff format 33 | language: system 34 | pass_filenames: false 35 | - id: ruff-check 36 | name: ruff 37 | description: Lint with ruff 38 | entry: uv run --frozen -q ruff check --fix --exit-non-zero-on-fix 39 | language: system 40 | pass_filenames: false 41 | - id: mypy-check 42 | name: mypy 43 | description: Check types with mypy 44 | entry: uv run --frozen -q mypy . 45 | language: system 46 | pass_filenames: false 47 | files: \.py$ 48 | - id: validate-exceptions 49 | name: validate exceptions 50 | description: Validate exceptions 51 | entry: python utils/validator.py 52 | language: system 53 | pass_filenames: false 54 | - id: py-test 55 | name: pytest 56 | stages: 57 | - "pre-push" 58 | description: Run pytest 59 | entry: uv run --frozen -q pytest -n auto --ff --exitfirst 60 | language: system 61 | pass_filenames: false 62 | -------------------------------------------------------------------------------- /tests/builddir/appstream-broken-icon/org.flathub.appstream_broken_icon.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.appstream_broken_icon 4 | Broken Icon 5 | Has a broken icon key in appstream 6 | CC0-1.0 7 | GPL-3.0-or-later 8 | 9 |

Has a broken icon key in appstream. A long description. An even longer description

10 |

Also has an icon with no type

11 |
12 | 13 | Flathub 14 | 15 | org.flathub.appstream_broken_icon.desktop 16 | https://example.org/icon1.png 17 | https://example.org/icon2.png 18 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 19 | org.flathub.appstream_broken_icon 20 | https://example.org 21 | 22 | Utility 23 | 24 | 25 | 26 | A test caption 27 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 29 | 30 | 31 | 32 | test 33 | 34 | 35 | 36 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /tests/builddir/appstream-no-icon-file/org.flathub.appstream_no_icon_file.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.appstream_no_icon_file 4 | Broken Icon 5 | Has a broken icon key in appstream 6 | CC0-1.0 7 | GPL-3.0-or-later 8 | 9 |

Has a broken icon key in appstream. A long description. An even longer description

10 |

Also has an icon with no type

11 |
12 | 13 | Flathub 14 | 15 | org.flathub.appstream_no_icon_file.desktop 16 | https://example.org/icon1.png 17 | https://example.org/icon2.png 18 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 19 | org.flathub.appstream_no_icon_file 20 | https://example.org 21 | 22 | Utility 23 | 24 | 25 | 26 | A test caption 27 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 29 | 30 | 31 | 32 | test 33 | 34 | 35 | 36 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /tests/builddir/appstream-icon-key-no-type/org.flathub.appstream_icon_key_no_type.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.appstream_icon_key_no_type 4 | Broken Icon 5 | Has a broken icon key in appstream 6 | CC0-1.0 7 | GPL-3.0-or-later 8 | 9 |

Has a broken icon key in appstream. A long description. An even longer description

10 |

Also has an icon with no type

11 |
12 | 13 | Flathub 14 | 15 | org.flathub.appstream_icon_key_no_type.desktop 16 | https://example.org/icon1.png 17 | https://example.org/icon2.png 18 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 19 | org.flathub.appstream_icon_key_no_type 20 | https://example.org 21 | 22 | Utility 23 | 24 | 25 | 26 | A test caption 27 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 29 | 30 | 31 | 32 | test 33 | 34 | 35 | 36 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /tests/builddir/appstream-broken-remote-icon/org.flathub.appstream_broken_remote_icon.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.flathub.appstream_broken_remote_icon 4 | Broken Icon 5 | Has a broken icon key in appstream 6 | CC0-1.0 7 | GPL-3.0-or-later 8 | 9 |

Has a broken icon key in appstream. A long description. An even longer description

10 |

Also has an icon with no type

11 |
12 | 13 | Flathub 14 | 15 | org.flathub.appstream_broken_remote_icon.desktop 16 | https://example.org/icon1.png 17 | https://example.org/icon2.png 18 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 19 | org.flathub.appstream_broken_remote_icon 20 | https://example.org 21 | 22 | Utility 23 | 24 | 25 | 26 | A test caption 27 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 29 | 30 | 31 | 32 | test 33 | 34 | 35 | 36 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /tests/builddir/misplaced-icons/org.flathub.example.misplaced-icons.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.example.misplaced-icons 5 | Foo Bar 6 | Foo foo foo foo 7 | Flathub 8 |

>An example desktop application

9 | org.flathub.example.misplaced-icons.png 10 | org.flathub.example.misplaced-icons.png 11 | 12 | Network 13 | 14 | GPL-2.0+ 15 | https://example.org 16 | 17 | 18 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 19 | https://dl.flathub.org/media/org.flathub.example.gui-master/624x351/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 20 | https://dl.flathub.org/media/org.flathub.example.gui-master/112x63/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 21 | https://dl.flathub.org/media/org.flathub.example.gui-master/224x126/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 22 | https://dl.flathub.org/media/org.flathub.example.gui-master/752x423/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.flathub.example.misplaced-icons.desktop 30 |
31 |
32 | -------------------------------------------------------------------------------- /tests/builddir/appstream-missing-timestamp/org.flathub.appstream_no_timestamp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream_no_timestamp 5 | Foo Bar 6 | Foo foo foo foo 7 | Flathub 8 |

>An example desktop application

9 | org.flathub.appstream_no_timestamp.png 10 | org.flathub.appstream_no_timestamp.png 11 | 12 | Network 13 | 14 | GPL-2.0+ 15 | https://example.org 16 | 17 | 18 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 19 | https://dl.flathub.org/media/org.flathub.example.gui-master/624x351/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 20 | https://dl.flathub.org/media/org.flathub.example.gui-master/112x63/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 21 | https://dl.flathub.org/media/org.flathub.example.gui-master/224x126/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 22 | https://dl.flathub.org/media/org.flathub.example.gui-master/752x423/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 23 | foobar 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.flathub.appstream_no_timestamp.desktop 31 |
32 |
33 | -------------------------------------------------------------------------------- /tests/manifests/finish_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id": "org.flathub.finish_args", 3 | "finish-args": [ 4 | "--device=all", 5 | "--nodevice=dri", 6 | "--filesystem=home", 7 | "--filesystem=host", 8 | "--filesystem=home/.config/autostart", 9 | "--filesystem=~/.config/systemd", 10 | "--filesystem=~/.local/share/applications", 11 | "--filesystem=~/.cache", 12 | "--filesystem=~/.local", 13 | "--filesystem=~/.ssh", 14 | "--filesystem=~/.gnupg", 15 | "--filesystem=/run/media:ro", 16 | "--filesystem=xdg-config/kdeglobals:ro", 17 | "--filesystem=~/.themes:ro", 18 | "--filesystem=home/.icons", 19 | "--filesystem=host-root", 20 | "--filesystem=~/.fonts:create", 21 | "--talk-name=org.flathub.finish_args", 22 | "--own-name=org.flathub.finish_args", 23 | "--own-name=org.kde.StatusNotifierItem", 24 | "--talk-name=org.freedesktop.impl.portal.PermissionStore", 25 | "--own-name=org.mpris.MediaPlayer2.org.flathub.finish_args", 26 | "--socket=session-bus", 27 | "--socket=wayland", 28 | "--socket=x11", 29 | "--socket=fallback-x11", 30 | "--socket=gpg-agent", 31 | "--socket=ssh-auth", 32 | "--nosocket=cups", 33 | "--talk-name=org.gtk.vfs", 34 | "--talk-name=org.freedesktop.Flatpak", 35 | "--talk-name=org.kde.*", 36 | "--talk-name=org.freedesktop.DBus.foo", 37 | "--system-talk-name=org.kde.*", 38 | "--own-name=org.gnome.*", 39 | "--talk-name=org.freedesktop.*", 40 | "--own-name=org.freedesktop.portal.Foo", 41 | "--system-talk-name=org.freedesktop.DBus.foo", 42 | "--unshare=network", 43 | "--filesystem=~/.var/app/foo", 44 | "--filesystem=/var/lib/flatpak", 45 | "--filesystem=/tmp", 46 | "--filesystem=/var/foobar", 47 | "--no-talk-name=org.foo.bar", 48 | "--talk-name=org.freedesktop.systemd1", 49 | "--own-name=org.kde.KWin", 50 | "--system-talk-name=org.kde.plasmashell", 51 | "--system-own-name=org.freedesktop.login1" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /tests/builddir/appstream-broken-icon/org.flathub.appstream_broken_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream_broken_icon 5 | Broken Icon 6 | Has a broken icon key in appstream 7 | CC0-1.0 8 | GPL-3.0-or-later 9 | 10 |

Has a broken icon key in appstream. A long description. An even longer description

11 |

Also has an icon with no type

12 |
13 | 14 | Flathub 15 | 16 | org.foo.test.desktop 17 | org.flathub.appstream_broken_icon.png 18 | org.flathub.appstream_broken_icon.png 19 | org.flathub.appstream_broken_icon.png 20 | https://example.org/icon1.png 21 | https://example.org/icon2.png 22 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 23 | org.flathub.appstream_broken_icon 24 | https://example.org 25 | 26 | 27 | A test caption 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 29 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 30 | 31 | 32 | 33 | test 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /tests/builddir/appstream-no-icon-file/org.flathub.appstream_no_icon_file.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream_no_icon_file 5 | Broken Icon 6 | Has a broken icon key in appstream 7 | CC0-1.0 8 | GPL-3.0-or-later 9 | 10 |

Has a broken icon key in appstream. A long description. An even longer description

11 |

Also has an icon with no type

12 |
13 | 14 | Flathub 15 | 16 | org.flathub.appstream_no_icon_file.desktop 17 | org.flathub.appstream_no_icon_file.png 18 | org.flathub.appstream_no_icon_file.png 19 | org.flathub.appstream_no_icon_file.png 20 | https://example.org/icon1.png 21 | https://example.org/icon2.png 22 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 23 | org.flathub.appstream_no_icon_file 24 | https://example.org 25 | 26 | 27 | A test caption 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 29 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 30 | 31 | 32 | 33 | test 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /flatpak_builder_lint/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | GITHUB_API = "https://api.github.com" 4 | GITHUB_CONTENT_CDN = "https://raw.githubusercontent.com" 5 | 6 | LINTER_FULL_REPO = "flathub-infra/flatpak-builder-lint" 7 | 8 | FLATHUB_REPO_BASE_URL = "https://dl.flathub.org" 9 | FLATHUB_API_URL = "https://flathub.org/api/v2" 10 | FLATHUB_MEDIA_BASE_URL = f"{FLATHUB_REPO_BASE_URL}/media" 11 | FLATHUB_STABLE_REPO_URL = f"{FLATHUB_REPO_BASE_URL}/repo" 12 | FLATHUB_BETA_REPO_URL = f"{FLATHUB_REPO_BASE_URL}/beta-repo" 13 | FLATHUB_BUILD_BASE_URL = "https://hub.flathub.org" 14 | FLATHUB_BUILD_API_URL = f"{FLATHUB_BUILD_BASE_URL}/api/v1" 15 | FLATHUB_GITHUB_ORG_URL = "https://github.com/flathub/" 16 | 17 | FLATHUB_SUPPORTED_ARCHES = ("x86_64", "aarch64") 18 | 19 | FLATHUB_RUNTIME_PREFIXES = ("org.freedesktop.", "org.gnome.", "org.kde.") 20 | FLATHUB_RUNTIME_SUFFIXES = (".Platform", ".Sdk") 21 | 22 | IGNORE_REF_SUFFIXES = (".Locale", ".Debug", ".Sources") 23 | 24 | FLATHUB_JSON_FILE = "flathub.json" 25 | 26 | FLATHUB_BASEAPP_IDENTIFIER = ".BaseApp" 27 | 28 | FLATHUB_APPSTREAM_TYPES_APPS = ( 29 | "desktop", 30 | "desktop-application", 31 | "console-application", 32 | ) 33 | 34 | FLATHUB_APPSTREAM_TYPES_DESKTOP = ( 35 | "desktop", 36 | "desktop-application", 37 | ) 38 | 39 | FLATHUB_APPSTREAM_TYPES_CONSOLE = "console-application" 40 | 41 | FLATHUB_APPSTREAM_TYPES = ( 42 | "generic", 43 | "addon", 44 | "runtime", 45 | *FLATHUB_APPSTREAM_TYPES_APPS, 46 | ) 47 | 48 | 49 | # Keep root URL path 50 | FLATHUB_ALLOWED_GITMODULE_URLS = ( 51 | FLATHUB_GITHUB_ORG_URL, 52 | "https://github.com/flathub-infra/", 53 | "https://github.com/flatpak/", 54 | "git@github.com:flathub/", 55 | "git@github.com:flatpak/", 56 | "git@github.com:flathub-infra/", 57 | ) 58 | 59 | XDG_CACHE_HOME = os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")) 60 | CACHEDIR = os.path.join(XDG_CACHE_HOME, "flatpak-builder-lint") 61 | 62 | 63 | def is_flathub_build_pipeline() -> bool: 64 | return os.getenv("REPO", "").startswith(FLATHUB_GITHUB_ORG_URL) 65 | 66 | 67 | def is_flatmgr_pipeline() -> bool: 68 | return bool(os.getenv("FLAT_MANAGER_BUILD_ID")) 69 | 70 | 71 | def is_flathub_pipeline() -> bool: 72 | return is_flathub_build_pipeline() or is_flatmgr_pipeline() 73 | -------------------------------------------------------------------------------- /tests/builddir/appstream-icon-key-no-type/org.flathub.appstream_icon_key_no_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream_icon_key_no_type 5 | Broken Icon 6 | Has a broken icon key in appstream 7 | CC0-1.0 8 | GPL-3.0-or-later 9 | 10 |

Has a broken icon key in appstream. A long description. An even longer description

11 |

Also has an icon with no type

12 |
13 | 14 | Flathub 15 | 16 | org.flathub.appstream_icon_key_no_type.desktop 17 | org.flathub.appstream_icon_key_no_type.png 18 | org.flathub.appstream_icon_key_no_type.png 19 | org.flathub.appstream_icon_key_no_type.png 20 | https://example.org/icon1.png 21 | https://example.org/icon2.png 22 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 23 | org.flathub.appstream_icon_key_no_type 24 | https://example.org 25 | 26 | 27 | A test caption 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 29 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 30 | 31 | 32 | 33 | test 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /tests/builddir/appstream-broken-remote-icon/org.flathub.appstream_broken_remote_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream_broken_remote_icon 5 | Broken Icon 6 | Has a broken icon key in appstream 7 | CC0-1.0 8 | GPL-3.0-or-later 9 | 10 |

Has a broken icon key in appstream. A long description. An even longer description

11 |

Also has an icon with no type

12 |
13 | 14 | Flathub 15 | 16 | org.flathub.appstream_broken_remote_icon.desktop 17 | org.flathub.appstream_broken_remote_icon.png 18 | org.flathub.appstream_broken_remote_icon.png 19 | org.flathub.appstream_broken_remote_icon.png 20 | https://example.org/icon1.png 21 | https://example.org/icon2.png 22 | https://dl.flathub.org/media/org/flathub/appstream_broken_icon/7934ba328ca1b4a3f35dade2bbc3073e/icons/128x128/org.flathub.appstream_broken_icon.png 23 | org.flathub.appstream_broken_remote_icon 24 | https://example.org 25 | 26 | 27 | A test caption 28 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_orig.png 29 | https://dl.flathub.org/media/org/flathub/broken/e59abd56b026a909d6d2d683ef0b07d6/screenshots/image-1_1248x700@1.png 30 | 31 | 32 | 33 | test 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /tests/builddir/appstream-manifest-url-unreachable/org.flathub.appstream_manifest_url_unreachable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.flathub.appstream_manifest_url_unreachable 5 | Foo Bar 6 | Foo foo foo foo 7 | Flathub 8 |

>An example desktop application

9 | org.flathub.appstream_manifest_url_unreachable.png 10 | org.flathub.appstream_manifest_url_unreachable.png 11 | 12 | Network 13 | 14 | GPL-2.0+ 15 | https://example.org 16 | 17 | 18 | https://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png 19 | https://dl.flathub.org/media/org.flathub.appstream_manifest_url_unreachable-master/624x351/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 20 | https://dl.flathub.org/media/org.flathub.appstream_manifest_url_unreachable-master/112x63/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 21 | https://dl.flathub.org/media/org.flathub.appstream_manifest_url_unreachable-master/224x126/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 22 | https://dl.flathub.org/media/org.flathub.appstream_manifest_url_unreachable-master/752x423/org.flathub.example.gui-5b97a2051866698942c536e94169b6a8.png 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.flathub.appstream_manifest_url_unreachable.desktop 30 | 31 | https://raw.githubusercontent.com/Ghost/wwww/master/org.flathub.appstream_manifest_url_unreachable.yml 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Trigger release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [CI] 6 | types: [completed] 7 | branches: [master] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | actions: read 16 | timeout-minutes: 20 17 | if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'flathub-infra/flatpak-builder-lint' && github.ref == 'refs/heads/master' }} 18 | steps: 19 | # 4.2.2 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 21 | with: 22 | persist-credentials: false 23 | ref: 'master' 24 | 25 | - name: Check last commit message 26 | id: check_commit 27 | run: | 28 | if git log -1 --pretty=%B | grep -E "\[release\]"; then 29 | echo "is_release=true" >> "$GITHUB_OUTPUT" 30 | fi 31 | 32 | - name: Check if docker-manifest succeeded 33 | if: steps.check_commit.outputs.is_release == 'true' 34 | id: check_job 35 | run: | 36 | job_status=$(gh run view ${{ github.event.workflow_run.id }} --json jobs --jq '.jobs[] | select(.name=="docker-call / docker-manifest") | .conclusion') 37 | echo "Status: $job_status" 38 | if [ "$job_status" == "success" ]; then 39 | echo "run=true" >> "$GITHUB_OUTPUT" 40 | fi 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Delay for 2 minutes 45 | if: steps.check_job.outputs.run == 'true' 46 | run: sleep 120 47 | 48 | - name: Trigger workflow in org.flatpak.Builder repository 49 | if: steps.check_job.outputs.run == 'true' 50 | # 3.0.0 51 | uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 52 | with: 53 | repository: flathub/org.flatpak.Builder 54 | event-type: trigger-workflow 55 | client-payload: '{"sha": "${{ github.sha }}"}' 56 | token: ${{ secrets.LINTER_TRIGGER_WORKFLOW_TOKEN }} 57 | 58 | - name: Trigger workflow in flathub-infra/actions-images 59 | if: steps.check_job.outputs.run == 'true' 60 | # 3.0.0 61 | uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 62 | with: 63 | repository: flathub-infra/actions-images 64 | event-type: trigger-workflow 65 | token: ${{ secrets.LINTER_TRIGGER_WORKFLOW_TOKEN }} 66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | include-package-data = true 7 | 8 | [tool.setuptools.packages.find] 9 | where = ["."] 10 | include = [ 11 | "flatpak_builder_lint", 12 | "flatpak_builder_lint.checks", 13 | "flatpak_builder_lint.staticfiles" 14 | ] 15 | exclude = ["**/__pycache__"] 16 | 17 | [tool.setuptools.package-data] 18 | flatpak_builder_lint = [ 19 | "staticfiles/flathub-stable.summary", 20 | "staticfiles/flathub-beta.summary", 21 | "staticfiles/*.json" 22 | ] 23 | 24 | [project] 25 | name = "flatpak_builder_lint" 26 | version = "3.0.0" 27 | description = "A linter for flatpak-builder manifests" 28 | authors = [ 29 | {name = "Bartłomiej Piotrowski", email = "b@bpiotrowski.pl"}, 30 | {name = "bbhtt", email = "bbhtt@bbhtt.in"}, 31 | ] 32 | license = {text = "MIT"} 33 | readme = "README.md" 34 | requires-python = "<4.0,>=3.10" 35 | dependencies = [ 36 | "jsonschema<5.0.0,>=4.23.0", 37 | "requests<3.0.0,>=2.32.2", 38 | "lxml<6.0.0,>=5.3.0", 39 | "sentry-sdk<3.0.0,>=2.8.0", 40 | "PyGObject<4.0.0,>=3.48.2", 41 | "requests-cache<2.0.0,>=1.2.1", 42 | "ruamel.yaml<1.0.0,>=0.18.14", 43 | "publicsuffixlist>=1.0.2.20250802,<2", 44 | ] 45 | 46 | [project.urls] 47 | Homepage = "https://github.com/flathub-infra/flatpak-builder-lint" 48 | Documentation = "https://docs.flathub.org/docs/for-app-authors/linter" 49 | Repository = "https://github.com/flathub-infra/flatpak-builder-lint.git" 50 | Issues = "https://github.com/flathub-infra/flatpak-builder-lint/issues" 51 | 52 | [project.scripts] 53 | flatpak-builder-lint = "flatpak_builder_lint.cli:main" 54 | 55 | [dependency-groups] 56 | dev = [ 57 | "pytest<9.0.0,>=8.3.3", 58 | "pytest-xdist>=3.8.0,<4.0.0", 59 | "mypy<2.0.0,>=1.11.2", 60 | "ruff<1.0.0,>=0.6.7", 61 | "pre-commit<4.0.0,>=3.8.0", 62 | "types-requests<3.0.0.0,>=2.32.0.20240914", 63 | "types-jsonschema<5.0.0.0,>=4.23.0.20240813", 64 | "types-lxml<2025.0.0,>=2024.9.16", 65 | "PyGObject-stubs<3.0.0,>=2.11.0", 66 | ] 67 | 68 | [tool.mypy] 69 | check_untyped_defs = true 70 | disallow_any_generics = true 71 | disallow_any_unimported = true 72 | disallow_incomplete_defs = true 73 | disallow_subclassing_any = true 74 | disallow_untyped_calls = true 75 | disallow_untyped_decorators = true 76 | disallow_untyped_defs = true 77 | extra_checks = true 78 | no_implicit_optional = true 79 | no_implicit_reexport = true 80 | show_error_codes = true 81 | strict = true 82 | strict_equality = true 83 | warn_redundant_casts = true 84 | warn_return_any = true 85 | warn_unused_configs = true 86 | warn_unused_ignores = true 87 | cache_dir = "~/.cache/mypy" 88 | 89 | [tool.ruff] 90 | line-length = 100 91 | include = ["*.py"] 92 | target-version = "py310" 93 | 94 | [tool.ruff.lint] 95 | select = [ 96 | "A", 97 | "ARG", 98 | "B", 99 | "C4", 100 | "DTZ", 101 | "E", 102 | "ERA", 103 | "F", 104 | "I", 105 | "ICN", 106 | "PIE", 107 | "PL", 108 | "Q", 109 | "RET", 110 | "RSE", 111 | "RUF", 112 | "S", 113 | "SIM", 114 | "T201", 115 | "UP", 116 | "W", 117 | ] 118 | 119 | ignore = [ 120 | "PLR2004", 121 | "PLR0911", 122 | "PLR0912", 123 | "PLR0913", 124 | "PLR0915", 125 | "S105", 126 | "S320", 127 | "S607", 128 | "S603", 129 | ] 130 | 131 | [tool.ruff.format] 132 | line-ending = "lf" 133 | quote-style = "double" 134 | 135 | [tool.ruff.lint.per-file-ignores] 136 | "tests/*" = ["S101"] 137 | 138 | [tool.pytest.ini_options] 139 | addopts = "--ignore=tests/repo --ignore=tests/test_httpserver.py" 140 | testpaths = [ 141 | "tests", 142 | ] 143 | -------------------------------------------------------------------------------- /tests/python3-pytest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python3-modules", 3 | "buildsystem": "simple", 4 | "build-commands": [], 5 | "modules": [ 6 | { 7 | "name": "python3-pytest", 8 | "buildsystem": "simple", 9 | "build-commands": [ 10 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pytest==8.3.3\" --no-build-isolation" 11 | ], 12 | "sources": [ 13 | { 14 | "type": "file", 15 | "url": "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", 16 | "sha256": "9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" 17 | }, 18 | { 19 | "type": "file", 20 | "url": "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", 21 | "sha256": "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" 22 | }, 23 | { 24 | "type": "file", 25 | "url": "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", 26 | "sha256": "a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" 27 | } 28 | ] 29 | }, 30 | { 31 | "name": "python3-pytest-xdist", 32 | "buildsystem": "simple", 33 | "build-commands": [ 34 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pytest-xdist==3.8.0\" --no-build-isolation" 35 | ], 36 | "sources": [ 37 | { 38 | "type": "file", 39 | "url": "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", 40 | "sha256": "26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc" 41 | }, 42 | { 43 | "type": "file", 44 | "url": "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", 45 | "sha256": "9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" 46 | }, 47 | { 48 | "type": "file", 49 | "url": "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", 50 | "sha256": "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" 51 | }, 52 | { 53 | "type": "file", 54 | "url": "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", 55 | "sha256": "a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" 56 | }, 57 | { 58 | "type": "file", 59 | "url": "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", 60 | "sha256": "202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88" 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/elfarch.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import logging 3 | import os 4 | import struct 5 | import tempfile 6 | 7 | from .. import builddir, ostree 8 | from . import Check 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def is_elf(fname: str) -> bool: 14 | if not os.path.isfile(fname): 15 | return False 16 | try: 17 | with open(fname, "br") as f: 18 | return f.read(4) == b"\x7fELF" 19 | except OSError as e: 20 | logger.debug("Failed to read file %s: %s: %s", fname, type(e).__name__, e) 21 | return False 22 | 23 | 24 | def find_elf_files(path: str) -> list[str]: 25 | return [file for file in glob.iglob(f"{path}/**", recursive=True) if is_elf(file)] 26 | 27 | 28 | def get_elf_arch(fname: str) -> str | None: 29 | if is_elf(fname): 30 | try: 31 | with open(fname, "rb") as f: 32 | f.seek(18) 33 | e_machine = struct.unpack(" dict[str, str]: 50 | return {file: arch for file in find_elf_files(path) if (arch := get_elf_arch(file)) is not None} 51 | 52 | 53 | class ELFArchCheck(Check): 54 | def _validate(self, path: str, ref: str) -> None: 55 | splits = ref.split("/") 56 | ref_arch = splits[1] 57 | 58 | elf_arches_dict: dict[str, str] = {} 59 | 60 | for subpath in ("files/lib", "files/bin"): 61 | fullpath = os.path.join(path, subpath) 62 | elf_arches_dict.update(collect_elf_arches(fullpath)) 63 | 64 | elf_arches = elf_arches_dict.values() 65 | 66 | if not (elf_arches_dict and elf_arches): 67 | return 68 | 69 | if len(set(elf_arches)) >= 2: 70 | self.errors.add("elf-arch-multiple-found") 71 | self.info.add(f"elf-arch-multiple-found: {list(elf_arches)}") 72 | 73 | if ref_arch not in elf_arches: 74 | self.errors.add("elf-arch-not-found") 75 | self.info.add( 76 | f"elf-arch-not-found: Ref arch is {ref_arch} but collected ELF arch is" 77 | + f" {list(elf_arches)}, {elf_arches_dict}" 78 | ) 79 | 80 | def check_build(self, path: str) -> None: 81 | stripped_ref = builddir.get_runtime(path) 82 | if not stripped_ref: 83 | return 84 | 85 | self._validate(path, stripped_ref) 86 | 87 | def check_repo(self, path: str) -> None: 88 | return 89 | self._populate_refs(path) 90 | refs = self.repo_primary_refs 91 | if not refs: 92 | return 93 | 94 | for ref in refs: 95 | stripped_ref = "/".join(ref.split("/")[1:]) 96 | 97 | with tempfile.TemporaryDirectory() as tmpdir: 98 | for subdir in ("bin", "lib"): 99 | os.makedirs(os.path.join(tmpdir, subdir), exist_ok=True) 100 | ostree.extract_subpath( 101 | path, ref, f"files/{subdir}", os.path.join(tmpdir, subdir), True 102 | ) 103 | 104 | self._validate(tmpdir, stripped_ref) 105 | -------------------------------------------------------------------------------- /flatpak_builder_lint/gitutils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import subprocess 5 | from functools import cache 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | @cache 11 | def is_git_directory(path: str) -> bool: 12 | if not os.path.exists(path): 13 | logger.debug("Failed to determine git directory as path does not exist: %s", path) 14 | return False 15 | 16 | result = subprocess.run( 17 | ["git", "rev-parse"], cwd=path, capture_output=True, text=True, check=False 18 | ) 19 | 20 | if result.returncode != 0: 21 | logger.debug( 22 | "Failed to determine git directory as git rev-parse failed with %s: %s", 23 | result.returncode, 24 | result.stderr.strip(), 25 | ) 26 | 27 | return result.returncode == 0 28 | 29 | 30 | @cache 31 | def get_git_toplevel(path: str) -> str | None: 32 | if not is_git_directory(path): 33 | return None 34 | 35 | result = subprocess.run( 36 | ["git", "rev-parse", "--show-toplevel"], 37 | cwd=path, 38 | capture_output=True, 39 | text=True, 40 | check=False, 41 | ) 42 | 43 | if result.returncode != 0: 44 | logger.debug( 45 | "Failed to get git toplevel with %s: %s", result.returncode, result.stderr.strip() 46 | ) 47 | return None 48 | 49 | return result.stdout.strip() 50 | 51 | 52 | @cache 53 | def get_github_repo_namespace(path: str) -> str | None: 54 | namespace = None 55 | 56 | if not is_git_directory(path): 57 | return None 58 | 59 | result = subprocess.run( 60 | ["git", "remote", "get-url", "origin"], 61 | cwd=path, 62 | capture_output=True, 63 | text=True, 64 | check=False, 65 | ) 66 | 67 | if result.returncode != 0: 68 | logger.debug( 69 | "Failed to get git remote URL with %s: %s", result.returncode, result.stderr.strip() 70 | ) 71 | return None 72 | 73 | remote_url = result.stdout.strip() 74 | 75 | https_pattern = r"https://github\.com/([^/]+/[^/.]+)" 76 | ssh_pattern = r"git@github\.com:([^/]+/[^.]+)" 77 | 78 | https_match = re.search(https_pattern, remote_url) 79 | ssh_match = re.search(ssh_pattern, remote_url) 80 | 81 | if https_match and "/" in https_match.group(1): 82 | namespace = https_match.group(1).split("/")[0] 83 | elif ssh_match and "/" in ssh_match.group(1): 84 | namespace = ssh_match.group(1).split("/")[0] 85 | 86 | if namespace is None: 87 | logger.debug("Failed to parse GitHub namespace from remote URL: %s", remote_url) 88 | else: 89 | logger.debug("GitHub namespace for remote %s is %s", remote_url, namespace) 90 | 91 | return namespace 92 | 93 | 94 | @cache 95 | def get_repo_tree_size(path: str) -> int: 96 | if not is_git_directory(path): 97 | return 0 98 | 99 | try: 100 | result = subprocess.run( 101 | ["git", "ls-tree", "-r", "-l", "HEAD"], 102 | cwd=path, 103 | capture_output=True, 104 | text=True, 105 | check=True, 106 | ) 107 | total = 0 108 | for line in result.stdout.splitlines(): 109 | parts = line.split() 110 | if len(parts) >= 4 and parts[-2].isdigit(): 111 | total += int(parts[-2]) 112 | 113 | logger.debug("Git repo tree size for %s: %s bytes", path, total) 114 | return total 115 | except subprocess.CalledProcessError as e: 116 | logger.debug( 117 | "Failed to get git repo tree size with %s: %s", 118 | e.returncode, 119 | e.stderr.strip(), 120 | ) 121 | return 0 122 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/toplevel.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from typing import Any 3 | 4 | from .. import config 5 | from . import Check 6 | 7 | 8 | class TopLevelCheck(Check): 9 | def check_manifest(self, manifest: Mapping[str, Any]) -> None: 10 | yaml_failed = manifest.get("x-manifest-yaml-failed") 11 | if yaml_failed: 12 | self.errors.add("manifest-invalid-yaml") 13 | self.info.add(f"manifest-invalid-yaml: {yaml_failed}") 14 | 15 | unknown_propeties = manifest.get("x-manifest-unknown-properties") 16 | 17 | if unknown_propeties: 18 | self.errors.add("manifest-unknown-properties") 19 | for p in unknown_propeties: 20 | self.info.add( 21 | f"manifest-unknown-properties: Invalid property " 22 | f"'{p.get('property')}' in context '{p.get('context')}'" 23 | ) 24 | 25 | json_warnings = manifest.get("x-manifest-json-warnings") 26 | 27 | if json_warnings: 28 | self.errors.add("manifest-json-warnings") 29 | self.info.add(f"manifest-json-warnings: {json_warnings}") 30 | 31 | if config.is_flathub_build_pipeline(): 32 | build_args = manifest.get("build-options", {}).get("build-args", []) 33 | if build_args and "--share=network" in build_args: 34 | self.errors.add("manifest-toplevel-build-network-access") 35 | 36 | build_extension = manifest.get("build-extension") 37 | build_runtime = manifest.get("build-runtime") 38 | appid = manifest.get("id") 39 | is_baseapp = bool( 40 | isinstance(appid, str) and appid.endswith(config.FLATHUB_BASEAPP_IDENTIFIER) 41 | ) 42 | 43 | if not any([build_extension, is_baseapp, build_runtime]): 44 | command = manifest.get("command") 45 | if not command: 46 | self.errors.add("toplevel-no-command") 47 | elif command.startswith("/"): 48 | self.errors.add("toplevel-command-is-path") 49 | self.info.add( 50 | "toplevel-command-is-path: Command in manifest is a path" 51 | + f" {command}. Please install the executable to" 52 | + " $FLATPAK_DEST/bin and change command to just the name" 53 | ) 54 | 55 | branch = manifest.get("branch") 56 | 57 | if branch: 58 | self.errors.add("toplevel-unnecessary-branch") 59 | self.info.add( 60 | "toplevel-unnecessary-branch: Please remove the toplevel" 61 | + " branch property in the manifest" 62 | ) 63 | 64 | cleanup = manifest.get("cleanup") 65 | if cleanup: 66 | for c in cleanup: 67 | if c == "/lib/debug" or c.startswith("/lib/debug/"): 68 | self.errors.add("toplevel-cleanup-debug") 69 | break 70 | 71 | if not manifest.get("modules"): 72 | self.errors.add("toplevel-no-modules") 73 | 74 | gitmodules = manifest.get("x-gitmodules", []) 75 | ext_gitmodules = [ 76 | m for m in gitmodules if not m.startswith(config.FLATHUB_ALLOWED_GITMODULE_URLS) 77 | ] 78 | if ext_gitmodules: 79 | self.errors.add("external-gitmodule-url-found") 80 | self.info.add( 81 | "external-gitmodule-url-found: Only flatpak, flathub, and flathub-infra " 82 | f"gitmodules are allowed in manifest git repo: {ext_gitmodules}" 83 | ) 84 | 85 | if manifest.get("x-manifest-dir-large"): 86 | self.errors.add("manifest-directory-too-large") 87 | self.info.add("manifest-directory-too-large: Manifest directory is more than 25 MB") 88 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/flathub_json.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from collections.abc import Mapping 3 | from typing import Any 4 | 5 | from .. import builddir, config, ostree 6 | from . import Check 7 | 8 | 9 | class FlathubJsonCheck(Check): 10 | arches = config.FLATHUB_SUPPORTED_ARCHES 11 | 12 | def _check_if_extra_data(self, modules: list[dict[str, Any]]) -> bool: 13 | for module in modules: 14 | if sources := module.get("sources"): 15 | for source in sources: 16 | if source.get("type") == "extra-data": 17 | return True 18 | 19 | if nested_modules := module.get("modules"): 20 | return self._check_if_extra_data(nested_modules) 21 | 22 | return False 23 | 24 | def _validate( 25 | self, 26 | appid: str, 27 | flathub_json: dict[str, str | bool | list[str]], 28 | is_extension: bool, 29 | ) -> None: 30 | is_baseapp = appid.endswith(config.FLATHUB_BASEAPP_IDENTIFIER) 31 | 32 | eol = flathub_json.get("end-of-life") 33 | eol_rebase = flathub_json.get("end-of-life-rebase") 34 | automerge = flathub_json.get("automerge-flathubbot-prs") 35 | skip_appstream = flathub_json.get("skip-appstream-check") 36 | only_arches = flathub_json.get("only-arches") 37 | skip_arches = flathub_json.get("skip-arches") 38 | 39 | if skip_appstream and not (is_extension or is_baseapp): 40 | self.errors.add("flathub-json-skip-appstream-check") 41 | 42 | if automerge: 43 | self.errors.add("flathub-json-automerge-enabled") 44 | 45 | if eol_rebase and not eol: 46 | self.errors.add("flathub-json-eol-rebase-without-message") 47 | 48 | if only_arches is not None and not isinstance(only_arches, bool) and len(only_arches) == 0: 49 | self.errors.add("flathub-json-only-arches-empty") 50 | 51 | if ( 52 | skip_arches is not None 53 | and isinstance(skip_arches, set) 54 | and len(set(self.arches).intersection(skip_arches)) == len(self.arches) 55 | ): 56 | self.errors.add("flathub-json-excluded-all-arches") 57 | 58 | def check_manifest(self, manifest: Mapping[str, Any]) -> None: 59 | flathub_json = manifest.get("x-flathub") 60 | appid = manifest.get("id") 61 | if not (flathub_json and appid): 62 | return 63 | 64 | is_extension = manifest.get("build-extension", False) 65 | 66 | self._validate(appid, flathub_json, is_extension) 67 | 68 | def check_build(self, path: str) -> None: 69 | appid, ref_type = builddir.infer_appid(path), builddir.infer_type(path) 70 | if not (appid and ref_type): 71 | return 72 | metadata = builddir.parse_metadata(path) 73 | if not metadata: 74 | return 75 | 76 | flathub_json = builddir.get_flathub_json(path) 77 | if not flathub_json: 78 | return 79 | 80 | self._validate(appid, flathub_json, ref_type != "app") 81 | 82 | def check_repo(self, path: str) -> None: 83 | self._populate_refs(path) 84 | refs = self.repo_primary_refs 85 | if not refs: 86 | return 87 | 88 | for ref in refs: 89 | appid = ref.split("/")[1] 90 | 91 | with tempfile.TemporaryDirectory() as tmpdir: 92 | ostree.extract_subpath(path, ref, "/metadata", tmpdir) 93 | metadata = builddir.parse_metadata(tmpdir) 94 | if not metadata: 95 | return 96 | flathub_json = ostree.get_flathub_json(path, ref, tmpdir) 97 | if not flathub_json: 98 | return 99 | self._validate(appid, flathub_json, False) 100 | -------------------------------------------------------------------------------- /flatpak_builder_lint/appstream.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from typing import TypedDict, cast 4 | 5 | from lxml import etree 6 | 7 | from . import config 8 | 9 | 10 | class SubprocessResult(TypedDict): 11 | stdout: str 12 | stderr: str 13 | returncode: int 14 | 15 | 16 | def validate(path: str, *args: str) -> SubprocessResult: 17 | if not os.path.isfile(path): 18 | raise FileNotFoundError("AppStream file not found") 19 | 20 | cmd = subprocess.run( 21 | ["appstreamcli", "validate", *args, path], 22 | capture_output=True, 23 | check=False, 24 | ) 25 | 26 | ret: SubprocessResult = { 27 | "stdout": cmd.stdout.decode("utf-8"), 28 | "stderr": cmd.stderr.decode("utf-8"), 29 | "returncode": cmd.returncode, 30 | } 31 | 32 | return ret 33 | 34 | 35 | def parse_xml(path: str) -> etree._ElementTree: 36 | if not os.path.isfile(path): 37 | raise FileNotFoundError(f"XML file not found: {path}") 38 | try: 39 | return etree.parse(path) 40 | except etree.XMLSyntaxError as e: 41 | raise RuntimeError(f"XML syntax error in {path}: {e}") from None 42 | 43 | 44 | def xpath_list(path: str, query: str) -> list[str]: 45 | tree = parse_xml(path) 46 | return cast(list[str], tree.xpath(query)) 47 | 48 | 49 | def is_present(path: str, query: str) -> bool: 50 | return bool(xpath_list(path, query)) 51 | 52 | 53 | def component_type(path: str) -> str: 54 | types = xpath_list(path, "//component/@type") 55 | return types[0] if types else "generic" 56 | 57 | 58 | def get_icon_filename(path: str) -> str | None: 59 | icons = xpath_list(path, "//icon[@type='cached']/text()") 60 | return icons[0] if icons else None 61 | 62 | 63 | # Boolean returns 64 | 65 | 66 | def is_categories_present(path: str) -> bool: 67 | return is_present(path, "//categories/category") 68 | 69 | 70 | def is_developer_name_present(path: str) -> bool: 71 | return is_present(path, "//developer[@id]/name/text()") or is_present( 72 | path, "//developer_name/text()" 73 | ) 74 | 75 | 76 | def is_project_license_present(path: str) -> bool: 77 | return is_present(path, "//project_license/text()") 78 | 79 | 80 | def has_icon_key(path: str) -> bool: 81 | return is_present(path, "//icon") 82 | 83 | 84 | def icon_no_type(path: str) -> bool: 85 | return is_present(path, "//icon[not(@type)]") 86 | 87 | 88 | def check_caption(path: str) -> bool: 89 | return not is_present(path, "//screenshot[not(caption/text()) or not(caption)]") 90 | 91 | 92 | def all_release_has_timestamp(path: str) -> bool: 93 | return not is_present(path, "//releases/release[not(@timestamp)]") 94 | 95 | 96 | def is_remote_icon_mirrored(path: str) -> bool: 97 | return all( 98 | icon.startswith(f"{config.FLATHUB_MEDIA_BASE_URL}/") 99 | for icon in xpath_list(path, "//icon[@type='remote']/text()") 100 | ) 101 | 102 | 103 | def is_valid_component_type(path: str) -> bool: 104 | return component_type(path) in config.FLATHUB_APPSTREAM_TYPES 105 | 106 | 107 | # List returns 108 | 109 | 110 | def components(path: str) -> list[str]: 111 | return xpath_list(path, "/components/component") 112 | 113 | 114 | def metainfo_components(path: str) -> list[str]: 115 | return xpath_list(path, "/component") 116 | 117 | 118 | def appstream_id(path: str) -> list[str]: 119 | return xpath_list(path, "//component/id/text()") 120 | 121 | 122 | def get_launchable(path: str) -> list[str]: 123 | return xpath_list(path, "//launchable[@type='desktop-id']/text()") 124 | 125 | 126 | def get_screenshot_images(path: str) -> list[str]: 127 | return xpath_list(path, "//screenshots/screenshot/image/text()") 128 | 129 | 130 | def get_manifest_key(path: str) -> list[str]: 131 | return xpath_list(path, "//custom/value[@key='flathub::manifest']/text()") + xpath_list( 132 | path, "//metadata/value[@key='flathub::manifest']/text()" 133 | ) 134 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/modules.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections.abc import Mapping 3 | from typing import Any 4 | 5 | from .. import config 6 | from . import Check 7 | 8 | 9 | def _is_git_commit_hash(s: str) -> bool: 10 | return re.match(r"[a-f0-9]{4,40}", s) is not None 11 | 12 | 13 | def _get_bundled_extensions_not_prefixed_with_appid(manifest: Mapping[str, Any]) -> list[str]: 14 | appid = manifest.get("id", "") 15 | extensions = manifest.get("add-extensions", {}) 16 | return [ 17 | ext_id 18 | for ext_id, ext in extensions.items() 19 | if ext.get("bundle") is True and not ext_id.startswith(appid) 20 | ] 21 | 22 | 23 | class ModuleCheck(Check): 24 | def check_source(self, module_name: str, source: dict[str, str]) -> None: 25 | source_type = source.get("type") 26 | dest_filename = source.get("dest-filename") 27 | src_url = source.get("url", "") 28 | 29 | if dest_filename and dest_filename.find("/") != -1: 30 | self.errors.add(f"module-{module_name}-source-dest-filename-is-path") 31 | 32 | if source_type in ("archive", "file"): 33 | if source.get("sha1") and not src_url.startswith( 34 | ("https://registry.npmjs.org/", "https://registry.yarnpkg.com/") 35 | ): 36 | self.errors.add(f"module-{module_name}-source-sha1-deprecated") 37 | if source.get("md5"): 38 | self.errors.add(f"module-{module_name}-source-md5-deprecated") 39 | 40 | if source_type == "git": 41 | commit = source.get("commit") 42 | branch = source.get("branch") 43 | tag = source.get("tag") 44 | url = source.get("url") 45 | 46 | if not url: 47 | self.errors.add(f"module-{module_name}-source-git-no-url") 48 | return 49 | 50 | if url and not url.startswith(("http://", "https://")): 51 | self.errors.add(f"module-{module_name}-source-git-url-not-http") 52 | 53 | if not any([commit, branch, tag]): 54 | self.errors.add(f"module-{module_name}-source-git-no-tag-commit-branch") 55 | return 56 | 57 | if branch and not _is_git_commit_hash(branch): 58 | self.errors.add(f"module-{module_name}-source-git-branch") 59 | 60 | def check_module(self, module: dict[str, Any]) -> None: 61 | name = module.get("name") 62 | 63 | if config.is_flathub_build_pipeline(): 64 | build_args = module.get("build-options", {}).get("build-args", []) 65 | if build_args and "--share=network" in build_args: 66 | self.errors.add(f"module-{name}-build-network-access") 67 | 68 | buildsystem = module.get("buildsystem", "autotools") 69 | 70 | if buildsystem == "autotools" and (config_opts := module.get("config-opts")): 71 | for opt in config_opts: 72 | if opt.startswith("--enable-debug") and not opt.endswith("=no"): 73 | self.errors.add(f"module-{name}-autotools-non-release-build") 74 | 75 | if sources := module.get("sources"): 76 | for source in sources: 77 | if name := module.get("name"): 78 | self.check_source(name, source) 79 | if "commit-query" in source.get("x-checker-data", {}): 80 | self.errors.add(f"module-{name}-checker-tracks-commits") 81 | 82 | if nested_modules := module.get("modules"): 83 | for nested_module in nested_modules: 84 | self.check_module(nested_module) 85 | 86 | cleanup = module.get("cleanup") 87 | if cleanup: 88 | for c in cleanup: 89 | if c == "/lib/debug" or c.startswith("/lib/debug/"): 90 | self.errors.add(f"module-{name}-cleanup-debug") 91 | break 92 | 93 | def check_manifest(self, manifest: Mapping[str, Any]) -> None: 94 | if manifest: 95 | for ext in _get_bundled_extensions_not_prefixed_with_appid(manifest): 96 | self.errors.add(f"appid-unprefixed-bundled-extension-{ext}") 97 | 98 | if modules := manifest.get("modules"): 99 | for module in modules: 100 | self.check_module(module) 101 | -------------------------------------------------------------------------------- /flatpak_builder_lint/ostree.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | from functools import cache 5 | 6 | import gi 7 | 8 | from . import config 9 | 10 | gi.require_version("OSTree", "1.0") 11 | from gi.repository import Gio, GLib, OSTree # noqa: E402 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def open_ostree_repo(repo_path: str) -> OSTree.Repo: 17 | if not os.path.exists(repo_path): 18 | raise FileNotFoundError(f"Could not find repo directory: {repo_path}") 19 | 20 | repo = OSTree.Repo.new(Gio.File.new_for_path(repo_path)) 21 | 22 | try: 23 | repo.open(None) 24 | except GLib.Error as err: 25 | raise GLib.Error("Failed to open OSTree repo") from err 26 | 27 | return repo 28 | 29 | 30 | @cache 31 | def get_refs(repo_path: str, ref_prefix: str | None) -> set[str]: 32 | repo = open_ostree_repo(repo_path) 33 | _, refs = repo.list_refs(ref_prefix, None) 34 | 35 | logger.debug("Found refs %s in repo %s", set(refs.keys()), os.path.abspath(repo_path)) 36 | return set(refs.keys()) 37 | 38 | 39 | @cache 40 | def get_all_refs_filtered(repo_path: str) -> set[str]: 41 | refs = get_refs(repo_path, None) 42 | 43 | return { 44 | r 45 | for r in refs 46 | if (parts := r.split("/")) 47 | and len(parts) == 4 48 | and parts[2] in config.FLATHUB_SUPPORTED_ARCHES 49 | and not parts[1].endswith(config.IGNORE_REF_SUFFIXES) 50 | } 51 | 52 | 53 | @cache 54 | def get_primary_refs(repo_path: str) -> set[str]: 55 | primary_refs = { 56 | r 57 | for r in get_refs(repo_path, None) 58 | if (parts := r.split("/")) 59 | and len(parts) == 4 60 | and parts[0] == "app" 61 | and parts[2] in config.FLATHUB_SUPPORTED_ARCHES 62 | } 63 | logger.debug("Found primary refs %s in repo %s", primary_refs, os.path.abspath(repo_path)) 64 | return primary_refs 65 | 66 | 67 | @cache 68 | def infer_appid(path: str) -> str | None: 69 | refs = get_primary_refs(path) 70 | if refs: 71 | # Assume refs share the same ref_id 72 | # refs with different ref_id in a 73 | # single repo is not a supported case 74 | ref = next(iter(refs)) 75 | return ref.split("/")[1] 76 | 77 | return None 78 | 79 | 80 | def extract_subpath( 81 | repo_path: str, 82 | ref: str, 83 | subpath: str, 84 | dest: str, 85 | should_pass: bool = False, 86 | ) -> None: 87 | repo = open_ostree_repo(repo_path) 88 | opts = OSTree.RepoCheckoutAtOptions() 89 | # https://gitlab.gnome.org/GNOME/pygobject/-/issues/639 90 | opts.mode = int(OSTree.RepoCheckoutMode.USER) # type: ignore 91 | opts.overwrite_mode = int(OSTree.RepoCheckoutOverwriteMode.ADD_FILES) # type: ignore 92 | opts.subpath = subpath 93 | 94 | _, rev = repo.resolve_rev(ref, True) 95 | 96 | # https://sourceware.org/git/?p=glibc.git;a=blob;f=io/fcntl.h;h=f157991782681caabe9bd7edb46ec205731965af;hb=HEAD#l149 97 | AT_FDCWD = -100 98 | if rev: 99 | if should_pass: 100 | try: 101 | logger.debug( 102 | "Checking out subpath %s with G_IO_ERROR_NOT_FOUND allowed " 103 | "for ref %s at dest %s", 104 | subpath, 105 | ref, 106 | dest, 107 | ) 108 | repo.checkout_at(opts, AT_FDCWD, dest, rev, None) 109 | except GLib.Error as err: 110 | if err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND): 111 | pass 112 | else: 113 | raise 114 | else: 115 | logger.debug("Checking out subpath %s for ref %s at dest %s", subpath, ref, dest) 116 | repo.checkout_at(opts, AT_FDCWD, dest, rev, None) 117 | 118 | 119 | def get_flathub_json(repo_path: str, ref: str, dest: str) -> dict[str, str | bool | list[str]]: 120 | flathubjsonfile = config.FLATHUB_JSON_FILE 121 | extract_subpath(repo_path, ref, f"/files/{flathubjsonfile}", dest, True) 122 | flathub_json_path = os.path.join(dest, flathubjsonfile) 123 | flathub_json: dict[str, str | bool | list[str]] = {} 124 | 125 | if os.path.exists(flathub_json_path): 126 | with open(flathub_json_path) as fp: 127 | flathub_json = json.load(fp) 128 | 129 | return flathub_json 130 | -------------------------------------------------------------------------------- /flatpak_builder_lint/builddir.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import json 3 | import os 4 | from collections import defaultdict 5 | from functools import cache 6 | from types import MappingProxyType 7 | 8 | from gi.repository import GLib 9 | 10 | from . import config 11 | 12 | 13 | @cache 14 | def parse_metadata(builddir: str) -> MappingProxyType[str, str | dict[str, set[str]]]: 15 | if not os.path.exists(builddir): 16 | raise OSError(errno.ENOENT, f"No such build directory: {builddir}") 17 | 18 | metadata_path = os.path.join(builddir, "metadata") 19 | if not os.path.exists(metadata_path): 20 | raise OSError(errno.ENOENT, f"No metadata file in build directory: {builddir}") 21 | 22 | key_file = GLib.KeyFile.new() 23 | key_file.load_from_file(metadata_path, GLib.KeyFileFlags.NONE) 24 | 25 | metadata: dict[str, str | dict[str, set[str]]] = {} 26 | 27 | group = key_file.get_start_group() 28 | if group is None: 29 | raise GLib.Error("Start group in metadata not found") 30 | 31 | if group == "Runtime": 32 | keys = key_file.get_keys(group)[0] 33 | if "runtime" in keys: 34 | metadata["runtime"] = key_file.get_value(group, "runtime") 35 | elif "sdk" in keys: 36 | metadata["runtime"] = key_file.get_value(group, "sdk") 37 | 38 | elif group == "Application": 39 | metadata["runtime"] = key_file.get_value(group, "runtime") 40 | 41 | metadata["type"] = group.lower() 42 | metadata["name"] = key_file.get_value(group, "name") 43 | 44 | environment: dict[str, set[str]] = defaultdict(set) 45 | permissions: dict[str, set[str]] = defaultdict(set) 46 | 47 | if ( 48 | key_file.has_group("Application") 49 | and "required-flatpak" in key_file.get_keys("Application")[0] 50 | ): 51 | permissions["required-flatpak"] = {key_file.get_value("Application", "required-flatpak")} 52 | 53 | if key_file.has_group("Context"): 54 | for key in key_file.get_keys("Context")[0]: 55 | permissions[key] = set(key_file.get_string_list("Context", key)) 56 | 57 | if key_file.has_group("Session Bus Policy"): 58 | for key in key_file.get_keys("Session Bus Policy")[0]: 59 | bus_val = key_file.get_value("Session Bus Policy", key) 60 | permissions[f"{bus_val}-name"].add(key) 61 | 62 | if key_file.has_group("System Bus Policy"): 63 | for key in key_file.get_keys("System Bus Policy")[0]: 64 | bus_val = key_file.get_value("System Bus Policy", key) 65 | permissions[f"system-{bus_val}-name"].add(key) 66 | 67 | if "shared" in permissions: 68 | permissions["share"] = permissions.pop("shared") 69 | 70 | if "filesystems" in permissions: 71 | permissions["filesystem"] = permissions.pop("filesystems") 72 | 73 | if "sockets" in permissions: 74 | permissions["socket"] = permissions.pop("sockets") 75 | 76 | if "x11" in permissions["socket"] and "fallback-x11" in permissions["socket"]: 77 | permissions["socket"].remove("x11") 78 | 79 | if "devices" in permissions: 80 | permissions["device"] = permissions.pop("devices") 81 | 82 | metadata["permissions"] = permissions 83 | 84 | if key_file.has_group("Environment"): 85 | for key in key_file.get_keys("Environment")[0]: 86 | environment[key] = set(key_file.get_string_list("Environment", key)) 87 | 88 | metadata["environment"] = environment 89 | 90 | if key_file.has_group("Extra Data"): 91 | metadata["extra-data"] = "yes" 92 | 93 | return MappingProxyType(metadata) 94 | 95 | 96 | @cache 97 | def infer_appid(path: str) -> str | None: 98 | metadata = parse_metadata(path) 99 | if metadata: 100 | name = metadata.get("name") 101 | if isinstance(name, str): 102 | return name 103 | return None 104 | 105 | 106 | @cache 107 | def infer_type(path: str) -> str: 108 | return "app" if parse_metadata(path).get("type") == "application" else "runtime" 109 | 110 | 111 | @cache 112 | def get_runtime(path: str) -> str | None: 113 | return runtime if isinstance(runtime := parse_metadata(path).get("runtime"), str) else None 114 | 115 | 116 | def get_flathub_json(path: str) -> dict[str, str | bool | list[str]]: 117 | flathub_json_path = f"{path}/files/{config.FLATHUB_JSON_FILE}" 118 | flathub_json: dict[str, str | bool | list[str]] = {} 119 | 120 | if os.path.exists(flathub_json_path): 121 | with open(flathub_json_path) as f: 122 | flathub_json = json.load(f) 123 | 124 | return flathub_json 125 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/metainfo.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import tempfile 4 | 5 | from ruamel.yaml import YAML 6 | from ruamel.yaml.error import YAMLError 7 | 8 | from .. import appstream, builddir, config, ostree 9 | from . import Check 10 | 11 | 12 | class MetainfoCheck(Check): 13 | def _validate(self, path: str, appid: str, ref_type: str) -> None: 14 | skip = False 15 | if appid.endswith(config.FLATHUB_BASEAPP_IDENTIFIER) or ref_type == "runtime": 16 | skip = True 17 | metainfo_dirs = [f"{path}/metainfo", f"{path}/appdata"] 18 | patterns = [ 19 | f"{appid}.metainfo.xml", 20 | f"{appid}.*.metainfo.xml", 21 | f"{appid}.appdata.xml", 22 | f"{appid}.*.appdata.xml", 23 | ] 24 | metainfo_files = [ 25 | os.path.abspath(file) 26 | for metainfo_dir in metainfo_dirs 27 | if os.path.isdir(metainfo_dir) 28 | for pattern in patterns 29 | for file in glob.glob(os.path.join(metainfo_dir, pattern)) 30 | if os.path.isfile(file) 31 | ] 32 | exact_metainfo = next( 33 | ( 34 | file 35 | for file in metainfo_files 36 | if file.endswith((f"{appid}.metainfo.xml", f"{appid}.appdata.xml")) 37 | ), 38 | None, 39 | ) 40 | 41 | if not skip and exact_metainfo is None: 42 | self.errors.add("appstream-metainfo-missing") 43 | self.info.add( 44 | f"appstream-metainfo-missing: No metainfo file for {appid} was found in" 45 | + " /app/share/metainfo or /app/share/appdata" 46 | ) 47 | return 48 | 49 | for file in metainfo_files: 50 | metainfo_validation = appstream.validate(file, "--no-net", "--format", "yaml") 51 | if metainfo_validation["returncode"] != 0: 52 | self.errors.add("appstream-failed-validation") 53 | self.info.add( 54 | f"appstream-failed-validation: Metainfo file {file} has failed" 55 | + " validation. Please see the errors in appstream block" 56 | ) 57 | 58 | for err in metainfo_validation["stderr"].splitlines(): 59 | self.appstream.add(err.strip()) 60 | 61 | yaml = YAML() 62 | 63 | try: 64 | validation_data = yaml.load(metainfo_validation["stdout"]) 65 | filename = validation_data.get("File", file) 66 | issues = validation_data.get("Issues", []) 67 | for issue in issues: 68 | severity = issue.get("severity", "").lower() 69 | if severity in ("warning", "error"): 70 | sev_prefix = "W" if severity == "warning" else "E" 71 | tag = issue.get("tag") 72 | line = issue.get("line") 73 | explanation = issue.get("explanation") 74 | parts = [sev_prefix, filename] 75 | if tag: 76 | parts.append(tag.strip()) 77 | if line: 78 | parts.append(str(line).strip()) 79 | message = ":".join(parts) 80 | if explanation: 81 | message += f" {explanation.strip()}" 82 | self.appstream.add(message.strip()) 83 | except YAMLError as e: 84 | self.appstream.add(f"Failed to parse appstream validate YAML output: {e}") 85 | 86 | if not appstream.metainfo_components(file): 87 | self.errors.add("metainfo-missing-component-tag") 88 | return 89 | 90 | def check_build(self, path: str) -> None: 91 | appid, ref_type = builddir.infer_appid(path), builddir.infer_type(path) 92 | if not (appid and ref_type): 93 | return 94 | 95 | self._validate(f"{path}/files/share", appid, ref_type) 96 | 97 | def check_repo(self, path: str) -> None: 98 | for ref in ostree.get_all_refs_filtered(path): 99 | parts = ref.split("/") 100 | ref_type, appid = parts[0], parts[1] 101 | 102 | if not (appid and ref_type): 103 | return 104 | 105 | with tempfile.TemporaryDirectory() as tmpdir: 106 | for subdir in ("appdata", "metainfo"): 107 | os.makedirs(os.path.join(tmpdir, subdir), exist_ok=True) 108 | ostree.extract_subpath( 109 | path, ref, f"files/share/{subdir}", os.path.join(tmpdir, subdir), True 110 | ) 111 | 112 | self._validate(tmpdir, appid, ref_type) 113 | -------------------------------------------------------------------------------- /flatpak_builder_lint/exceptions_janitor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import requests 5 | 6 | from . import config, domainutils 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | ISSUE_TITLE = "Stale exceptions" 11 | ISSUE_LABEL = "stale-exceptions" 12 | 13 | 14 | def get_stale_exceptions(active_errors: set[str], exceptions: set[str]) -> set[str]: 15 | stale: set[str] = set() 16 | 17 | for exception in exceptions: 18 | if exception == "*": 19 | continue 20 | 21 | if exception.startswith( 22 | ( 23 | "flathub-json-", 24 | "module-", 25 | "appid-unprefixed-bundled-extension-", 26 | "external-gitmodule-url-found", 27 | "manifest-", 28 | "toplevel-", 29 | ) 30 | ): 31 | continue 32 | 33 | if exception not in active_errors: 34 | stale.add(exception) 35 | 36 | return stale 37 | 38 | 39 | def report_stale_exceptions(appid: str, stale_exceptions: set[str]) -> bool: 40 | if not stale_exceptions: 41 | return True 42 | 43 | github_token = os.getenv("GITHUB_TOKEN") 44 | if not github_token: 45 | logger.debug("No GITHUB_TOKEN found, cannot report stale exceptions") 46 | return False 47 | 48 | headers = { 49 | "Authorization": f"token {github_token}", 50 | "Accept": "application/vnd.github.v3+json", 51 | } 52 | 53 | try: 54 | url_issues = f"{config.GITHUB_API}/repos/{config.LINTER_FULL_REPO}/issues" 55 | response = requests.get( 56 | url_issues, 57 | headers=headers, 58 | params={"state": "open", "creator": "flathubbot", "labels": ISSUE_LABEL}, 59 | timeout=30, 60 | ) 61 | logger.debug( 62 | "Request headers for %s: %s", 63 | url_issues, 64 | domainutils.filter_request_headers(dict(response.request.headers)), 65 | ) 66 | logger.debug("Response headers for %s: %s", url_issues, dict(response.headers)) 67 | response.raise_for_status() 68 | issues = response.json() 69 | 70 | existing_issue = next((i for i in issues if i["title"] == ISSUE_TITLE), None) 71 | exception_list = "\n".join(f"- {exc}" for exc in sorted(stale_exceptions)) 72 | issue_body = f"Stale exceptions for `{appid}`:\n\n{exception_list}" 73 | 74 | if existing_issue: 75 | issue_number = existing_issue["number"] 76 | 77 | comments_url = ( 78 | f"{config.GITHUB_API}/repos/" 79 | f"{config.LINTER_FULL_REPO}/issues/" 80 | f"{issue_number}/comments" 81 | ) 82 | 83 | comments_resp = requests.get(comments_url, headers=headers, timeout=30) 84 | logger.debug( 85 | "Request headers for %s: %s", 86 | comments_url, 87 | domainutils.filter_request_headers(dict(comments_resp.request.headers)), 88 | ) 89 | logger.debug("Response headers for %s: %s", comments_url, dict(comments_resp.headers)) 90 | comments_resp.raise_for_status() 91 | comments = comments_resp.json() 92 | 93 | if any(comment["body"] == issue_body for comment in comments): 94 | logger.debug("Comment already exists for %s, skipping", appid) 95 | return True 96 | 97 | post_resp = requests.post( 98 | comments_url, headers=headers, json={"body": issue_body}, timeout=30 99 | ) 100 | logger.debug( 101 | "Request headers for %s: %s", 102 | comments_url, 103 | domainutils.filter_request_headers(dict(post_resp.request.headers)), 104 | ) 105 | logger.debug("Response headers for %s: %s", comments_url, dict(post_resp.headers)) 106 | post_resp.raise_for_status() 107 | return True 108 | 109 | create_url = f"{config.GITHUB_API}/repos/{config.LINTER_FULL_REPO}/issues" 110 | create_resp = requests.post( 111 | create_url, 112 | headers=headers, 113 | json={"title": ISSUE_TITLE, "body": issue_body, "labels": [ISSUE_LABEL]}, 114 | timeout=30, 115 | ) 116 | logger.debug( 117 | "Request headers for %s: %s", 118 | create_url, 119 | domainutils.filter_request_headers(dict(create_resp.request.headers)), 120 | ) 121 | logger.debug("Response headers for %s: %s", create_url, dict(create_resp.headers)) 122 | create_resp.raise_for_status() 123 | return True 124 | 125 | except requests.exceptions.RequestException as e: 126 | logger.debug( 127 | "Request exception when reporting stale exceptions for %s: %s: %s", 128 | appid, 129 | type(e).__name__, 130 | e, 131 | ) 132 | return False 133 | -------------------------------------------------------------------------------- /tests/test_httpserver.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import signal 5 | import threading 6 | from http.server import BaseHTTPRequestHandler, HTTPServer 7 | 8 | 9 | class CustomHandler(BaseHTTPRequestHandler): 10 | def do_GET(self) -> None: 11 | if self.path == "/api/v1/build/0/extended": 12 | self.send_response(200) 13 | self.send_header("Content-type", "application/json") 14 | self.end_headers() 15 | json_string = { 16 | "build": { 17 | "app_id": None, 18 | "build_log_url": "https://buildbot.flathub.org/#/builders/6/builds/8406", 19 | "commit_job_id": 107573, 20 | "created": "2023-11-28T11:43:58.274070", 21 | "extra_ids": [], 22 | "id": 66707, 23 | "public_download": True, 24 | "publish_job_id": 107683, 25 | "published_state": 2, 26 | "repo": "stable", 27 | "repo_state": 2, 28 | "token_branches": [], 29 | "token_name": "default", 30 | "token_type": "app", 31 | }, 32 | "build_refs": [ 33 | { 34 | "build_id": 66707, 35 | "build_log_url": None, 36 | "commit": "03cb80951512747ff4732ec91fbdb66f135646b78dd844e4f0c383e0e4961545", # noqa: E501 37 | "id": 432660, 38 | "ref_name": "screenshots/x86_64", 39 | }, 40 | { 41 | "build_id": 66707, 42 | "build_log_url": None, 43 | "commit": "98ff535bac32c1c76c4898a9d93508de66db10f55564812385fdacb010864956", # noqa: E501 44 | "id": 432661, 45 | "ref_name": "runtime/org.flathub.gui.Debug/x86_64/stable", 46 | }, 47 | { 48 | "build_id": 66707, 49 | "build_log_url": None, 50 | "commit": "e08164425b36bb3bc79acef9a40ea3bac7b19862adb596a38f0242b4b4b408ed", # noqa: E501 51 | "id": 432662, 52 | "ref_name": "runtime/org.flathub.gui.Locale/x86_64/stable", 53 | }, 54 | { 55 | "build_id": 66707, 56 | "build_log_url": None, 57 | "commit": "57c6246315ff6c494e8d657834a5137c8dbd96845e9e9d04a012448a1306fd5d", # noqa: E501 58 | "id": 432663, 59 | "ref_name": "app/org.flathub.gui/x86_64/stable", 60 | }, 61 | { 62 | "build_id": 66707, 63 | "build_log_url": None, 64 | "commit": "7c3ff51a95ebc2a4f4ee29303731b4d8b56222dcc0d4b509553952336ee2d84e", # noqa: E501 65 | "id": 432664, 66 | "ref_name": "runtime/org.flathub.gui.Sources/x86_64/stable", 67 | }, 68 | ], 69 | } 70 | self.wfile.write(json.dumps(json_string).encode("utf-8")) 71 | else: 72 | self.send_response(404) 73 | self.end_headers() 74 | self.wfile.write(b"Not Found") 75 | 76 | 77 | def run(server_class=HTTPServer, handler_class=CustomHandler, port=9001) -> None: # type: ignore 78 | server_address = ("", port) 79 | httpd = server_class(server_address, handler_class) 80 | try: 81 | httpd.serve_forever() 82 | except Exception as e: 83 | raise (e) 84 | finally: 85 | httpd.server_close() 86 | 87 | 88 | def main() -> None: 89 | parser = argparse.ArgumentParser() 90 | parser.add_argument("--stop", action="store_true", help="Stop the server") 91 | 92 | args = parser.parse_args() 93 | 94 | if args.stop: 95 | pid_file = "server.pid" 96 | if os.path.exists(pid_file): 97 | with open(pid_file) as f: 98 | pid = int(f.read().strip()) 99 | try: 100 | os.kill(pid, 0) 101 | os.kill(pid, signal.SIGKILL) 102 | print(f"Process {pid} terminated") # noqa: T201 103 | except ProcessLookupError: 104 | print(f"No process found with PID {pid}") # noqa: T201 105 | except PermissionError: 106 | print(f"Permission denied to terminate process {pid}") # noqa: T201 107 | os.unlink(pid_file) 108 | else: 109 | print("No PID file found") # noqa: T201 110 | return 111 | 112 | pid = os.getpid() 113 | if os.path.exists("server.pid"): 114 | os.unlink("server.pid") 115 | with open("server.pid", "w") as f: 116 | f.write(str(pid)) 117 | 118 | server_thread = threading.Thread(target=run, args=(HTTPServer, CustomHandler, 9001)) 119 | server_thread.start() 120 | 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/appid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from collections.abc import Mapping 4 | from typing import Any 5 | 6 | from .. import builddir, config, domainutils 7 | from . import Check 8 | 9 | 10 | class AppIDCheck(Check): 11 | def _validate(self, appid: str | None, is_extension: bool) -> None: 12 | if not appid: 13 | self.errors.add("appid-not-defined") 14 | return 15 | 16 | if len(appid) > 255: 17 | self.errors.add("appid-length-more-than-255-chars") 18 | return 19 | 20 | split = appid.split(".") 21 | 22 | if len(split) < 3: 23 | self.errors.add("appid-less-than-3-components") 24 | return 25 | 26 | if not all(re.match("^[A-Za-z_][\\w\\-]*$", sp) for sp in split): 27 | self.errors.add("appid-component-wrong-syntax") 28 | return 29 | 30 | is_baseapp = appid.endswith(config.FLATHUB_BASEAPP_IDENTIFIER) 31 | 32 | if not (is_extension or is_baseapp) and len(split) > 5: 33 | self.errors.add("appid-too-many-components-for-app") 34 | self.info.add( 35 | "appid-too-many-components-for-app: appid has more than 5" 36 | + " components for an app" 37 | ) 38 | return 39 | 40 | if split[-1] == "desktop": 41 | self.errors.add("appid-ends-with-lowercase-desktop") 42 | 43 | domain = split[1].lower() 44 | tld = split[0].lower() 45 | if domain in ("github", "gitlab", "codeberg"): 46 | if tld != "io" and domain in ("github", "gitlab"): 47 | self.errors.add("appid-uses-code-hosting-domain") 48 | self.info.add(f"appid-uses-code-hosting-domain: {domain}.{tld}") 49 | return 50 | if tld != "page" and domain == "codeberg": 51 | self.errors.add("appid-uses-code-hosting-domain") 52 | self.info.add(f"appid-uses-code-hosting-domain: {domain}.{tld}") 53 | return 54 | if len(split) < 4: 55 | self.errors.add("appid-code-hosting-too-few-components") 56 | return 57 | 58 | if appid: 59 | if is_extension or is_baseapp: 60 | return 61 | if domainutils.is_app_on_flathub_summary(appid): 62 | return 63 | if appid.startswith(domainutils.CODE_HOSTS): 64 | proj_url = domainutils.get_proj_url(appid) 65 | if proj_url is None: 66 | self.errors.add("appid-url-check-internal-error") 67 | return 68 | url = f"https://{proj_url}" 69 | ok, resp = domainutils.check_url(url, strict=True) 70 | if not ok: 71 | self.errors.add("appid-url-not-reachable") 72 | message = f"appid-url-not-reachable: Tried {url}" 73 | if resp: 74 | message += f" | {resp}" 75 | self.info.add(message) 76 | else: 77 | domain_from_appid = domainutils.get_domain(appid) 78 | if domain_from_appid is None: 79 | self.errors.add("appid-url-check-internal-error") 80 | return 81 | url_https = f"https://{domain_from_appid}" 82 | ok, resp = domainutils.check_url(url_https, strict=False) 83 | if not ok: 84 | self.errors.add("appid-url-not-reachable") 85 | message = f"appid-url-not-reachable: Tried {url_https}" 86 | if resp: 87 | message += f" | {resp}" 88 | self.info.add(message) 89 | 90 | def check_manifest(self, manifest: Mapping[str, Any]) -> None: 91 | appid = manifest.get("id") 92 | is_extension = manifest.get("build-extension", False) 93 | 94 | if filename := manifest.get("x-manifest-filename"): 95 | (manifest_basename, _) = os.path.splitext(filename) 96 | manifest_basename = os.path.basename(manifest_basename) 97 | 98 | if os.path.exists(filename) and os.path.islink(filename): 99 | self.errors.add("manifest-file-is-symlink") 100 | 101 | if appid != manifest_basename: 102 | self.errors.add("appid-filename-mismatch") 103 | self.info.add( 104 | f"appid-filename-mismatch: Appid is {appid} but" 105 | + f" Manifest filename is: {manifest_basename}" 106 | ) 107 | 108 | self._validate(appid, is_extension) 109 | 110 | def check_build(self, path: str) -> None: 111 | appid, ref_type = builddir.infer_appid(path), builddir.infer_type(path) 112 | if not (appid and ref_type): 113 | return 114 | 115 | self._validate(appid, ref_type != "app") 116 | 117 | def check_repo(self, path: str) -> None: 118 | self._populate_refs(path) 119 | refs = self.repo_primary_refs 120 | if not refs: 121 | return 122 | 123 | for ref in refs: 124 | appid = ref.split("/")[1] 125 | self._validate(appid, False) 126 | -------------------------------------------------------------------------------- /flatpak_builder_lint/manifest.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import json 3 | import os 4 | import re 5 | import subprocess 6 | from functools import cache 7 | from types import MappingProxyType 8 | from typing import Any 9 | 10 | from ruamel.yaml import YAML 11 | from ruamel.yaml.comments import CommentedMap 12 | from ruamel.yaml.error import YAMLError 13 | 14 | from . import config, gitutils 15 | 16 | 17 | def format_yaml_error(e: YAMLError) -> str: 18 | err_type = f"{type(e).__module__}.{type(e).__name__}" 19 | msg = " ".join( 20 | line.strip() 21 | for line in str(e).splitlines() 22 | if not line.strip().startswith("To suppress this check") 23 | and not line.strip().startswith("https://yaml.dev/") 24 | ) 25 | return f"{err_type} {msg}" 26 | 27 | 28 | def get_key_lineno(manifest_path: str, key: str) -> int | None: 29 | if not os.path.isfile(manifest_path): 30 | return None 31 | 32 | if manifest_path.lower().endswith(".json"): 33 | try: 34 | with open(manifest_path, encoding="utf-8") as f: 35 | for i, line in enumerate(f, start=1): 36 | if f'"{key}"' in line or f"'{key}'" in line: 37 | return i 38 | except (OSError, json.JSONDecodeError): 39 | return None 40 | 41 | elif manifest_path.lower().endswith((".yaml", ".yml")): 42 | yaml = YAML() 43 | yaml.preserve_quotes = True 44 | try: 45 | with open(manifest_path, encoding="utf-8") as f: 46 | data = yaml.load(f) 47 | except (OSError, YAMLError): 48 | return None 49 | 50 | if isinstance(data, CommentedMap) and key in data: 51 | node = data.ca.items.get(key) 52 | if node and node[0] and hasattr(node[0], "start_mark"): 53 | return int(node[0].start_mark.line + 1) 54 | 55 | try: 56 | with open(manifest_path, encoding="utf-8") as f: 57 | for i, line in enumerate(f, start=1): 58 | if key in line: 59 | return i 60 | except OSError: 61 | return None 62 | 63 | return None 64 | 65 | 66 | # json-glib supports non-standard syntax like // comments. Bail out and 67 | # delegate parsing to flatpak-builder. This also gives us an easy support 68 | # for modules stored in external files. 69 | @cache 70 | def show_manifest(filename: str) -> MappingProxyType[str, Any]: 71 | if not os.path.exists(filename): 72 | raise OSError(errno.ENOENT, f"No such manifest file: {filename}") 73 | 74 | yaml_err: str | None = None 75 | if os.path.basename(filename).lower().endswith((".yaml", ".yml")): 76 | try: 77 | with open(filename) as f: 78 | YAML(typ="safe").load(f) 79 | except YAMLError as err: 80 | yaml_err = format_yaml_error(err).strip() 81 | 82 | ret = subprocess.run( 83 | ["flatpak-builder", "--show-manifest", filename], 84 | capture_output=True, 85 | check=False, 86 | env={**os.environ, "G_MESSAGES_PREFIXED": "all"}, 87 | ) 88 | 89 | stderr_s = ret.stderr.decode("utf-8") 90 | stdout_s = ret.stdout.decode("utf-8") 91 | 92 | unknown_pat = re.compile( 93 | r"^\*\* \(flatpak-builder:\d+\): WARNING \*\*: " 94 | r"[\d:.]+: Unknown property (\S+) for type (\S+)" 95 | ) 96 | 97 | json_warning_pat = re.compile(r"^\(flatpak-builder:\d+\): Json-WARNING \*\*: " r"[\d:.]+: (.+)") 98 | 99 | unknown_properties: list[dict[str, str]] = [] 100 | json_warnings: list[str] = [] 101 | 102 | if stderr_s: 103 | for line in stderr_s.splitlines(): 104 | if m := unknown_pat.match(line): 105 | type_name = m.group(2).strip() 106 | context = ( 107 | "source" 108 | if type_name == "BuilderSource" 109 | else "source-" + type_name[13:].lower() 110 | if type_name.startswith("BuilderSource") 111 | else type_name[7:].lower() 112 | if type_name.startswith("Builder") 113 | else type_name 114 | ) 115 | unknown_properties.append({"property": m.group(1).strip(), "context": context}) 116 | elif m := json_warning_pat.match(line): 117 | json_warnings.append(m.group(1).strip()) 118 | 119 | if ret.returncode != 0: 120 | raise Exception(stderr_s) 121 | 122 | manifest = stdout_s 123 | manifest_json: dict[str, Any] = json.loads(manifest) 124 | manifest_json["x-manifest-filename"] = filename 125 | 126 | if unknown_properties: 127 | manifest_json["x-manifest-unknown-properties"] = list( 128 | {(p["property"], p["context"]): p for p in unknown_properties}.values() 129 | ) 130 | 131 | if json_warnings: 132 | manifest_json["x-manifest-json-warnings"] = json_warnings 133 | 134 | if yaml_err: 135 | manifest_json["x-manifest-yaml-failed"] = yaml_err 136 | 137 | manifest_basedir = os.path.dirname(os.path.abspath(filename)) 138 | git_toplevel = gitutils.get_git_toplevel(manifest_basedir) 139 | flathub_json_path = os.path.join(manifest_basedir, config.FLATHUB_JSON_FILE) 140 | gitmodules_path = os.path.join(manifest_basedir, ".gitmodules") 141 | 142 | if git_toplevel is not None: 143 | gitmodules_path = os.path.join(git_toplevel, ".gitmodules") 144 | 145 | if os.path.exists(flathub_json_path): 146 | with open(flathub_json_path) as f: 147 | flathub_json = json.load(f) 148 | manifest_json["x-flathub"] = flathub_json 149 | 150 | github_ns = gitutils.get_github_repo_namespace(manifest_basedir) 151 | 152 | if github_ns in ("flathub", "flathub-infra"): 153 | if gitutils.get_repo_tree_size(manifest_basedir) > (25 * 1024 * 1024): 154 | manifest_json["x-manifest-dir-large"] = True 155 | 156 | if os.path.exists(gitmodules_path): 157 | with open(gitmodules_path) as f: 158 | manifest_json["x-gitmodules"] = [ 159 | line.split("=", 1)[1].strip() 160 | for line in f.readlines() 161 | if line.strip().startswith("url") 162 | and not line.split("=", 1)[1].strip().startswith(("./", "../")) 163 | ] 164 | 165 | return MappingProxyType(manifest_json) 166 | 167 | 168 | def infer_appid(path: str) -> str | None: 169 | manifest = show_manifest(path) 170 | return manifest.get("id") 171 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/flatmanager.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | import logging 4 | import os 5 | import shutil 6 | import tempfile 7 | 8 | import requests 9 | 10 | from .. import appstream, config, domainutils 11 | from . import Check 12 | 13 | REQUEST_TIMEOUT = domainutils.REQUEST_TIMEOUT 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class FlatManagerCheck(Check): 19 | def check_repo(self, path: str) -> None: 20 | flathub_hooks_cfg_paths = [ 21 | "/run/host/etc/flathub-hooks.json", 22 | "/etc/flathub-hooks.json", 23 | ] 24 | 25 | if build_id := os.getenv("FLAT_MANAGER_BUILD_ID"): 26 | flathub_hooks_cfg = {} 27 | for flathub_hooks_cfg_path in flathub_hooks_cfg_paths: 28 | if os.path.exists(flathub_hooks_cfg_path): 29 | with open(flathub_hooks_cfg_path) as f: 30 | flathub_hooks_cfg = json.load(f) 31 | break 32 | 33 | flatmgr_url = os.getenv("FLAT_MANAGER_URL") 34 | if not flatmgr_url: 35 | flatmgr_url = flathub_hooks_cfg.get("flat_manager_url") 36 | if not flatmgr_url: 37 | raise RuntimeError("No flat-manager URL configured") 38 | 39 | flatmgr_token = os.getenv("FLAT_MANAGER_TOKEN") 40 | if not flatmgr_token: 41 | flatmgr_token = flathub_hooks_cfg.get("flat_manager_token") 42 | if not flatmgr_token: 43 | raise RuntimeError("No flat-manager token configured") 44 | 45 | headers = { 46 | "Authorization": f"Bearer {flatmgr_token}", 47 | "Content-Type": "application/json", 48 | } 49 | 50 | flat_mgr_extended_api = f"{flatmgr_url}/api/v1/build/{build_id}/extended" 51 | r = requests.get( 52 | flat_mgr_extended_api, 53 | headers=headers, 54 | timeout=REQUEST_TIMEOUT, 55 | ) 56 | logger.debug( 57 | "Request headers for %s: %s", 58 | flat_mgr_extended_api, 59 | domainutils.filter_request_headers(dict(r.request.headers)), 60 | ) 61 | logger.debug("Response headers for %s: %s", flat_mgr_extended_api, dict(r.headers)) 62 | 63 | if r.status_code != 200: 64 | raise RuntimeError(f"Failed to fetch build info from flat-manager: {r.status_code}") 65 | 66 | build_extended = r.json() 67 | build_info = build_extended["build"] 68 | token_type = build_info["token_type"] 69 | target_repo = build_info["repo"] 70 | 71 | refs = [build_ref["ref_name"] for build_ref in build_extended.get("build_refs", [])] 72 | arches = {ref.split("/")[2] for ref in refs if len(ref.split("/")) == 4} 73 | 74 | if token_type == "app": 75 | has_app_ref = any(ref.startswith("app/") for ref in refs) 76 | if not has_app_ref: 77 | self.errors.add("flat-manager-no-app-ref-uploaded") 78 | return 79 | 80 | for ref in refs: 81 | if ref.startswith("screenshots/"): 82 | continue 83 | ref_branch = ref.split("/")[-1] 84 | if ref_branch != target_repo: 85 | self.errors.add("flat-manager-branch-repo-mismatch") 86 | break 87 | 88 | with tempfile.TemporaryDirectory() as tmpdir: 89 | with ( 90 | gzip.open( 91 | f"{path}/appstream/{arches.pop()}/appstream.xml.gz", "rb" 92 | ) as appstream_gz, 93 | open(f"{tmpdir}/appstream.xml", "wb") as appstream_file, 94 | ): 95 | shutil.copyfileobj(appstream_gz, appstream_file) 96 | 97 | manifest_key = appstream.get_manifest_key(f"{tmpdir}/appstream.xml") 98 | if not manifest_key: 99 | self.errors.add("appstream-no-flathub-manifest-key") 100 | 101 | else: 102 | ref_branches: set[str] = { 103 | parts[-1] 104 | for ref in refs 105 | if not ref.startswith( 106 | ( 107 | "runtime/org.freedesktop.Platform.GL.", 108 | "runtime/org.freedesktop.Platform.GL32.", 109 | ) 110 | ) 111 | and (parts := ref.strip("/").split("/")) 112 | and len(parts) >= 3 113 | } 114 | 115 | if not ref_branches: 116 | return 117 | 118 | all_branches_beta = all( 119 | branch.endswith(("beta", "beta-extra")) for branch in ref_branches 120 | ) 121 | 122 | any_branches_beta = any( 123 | branch.endswith(("beta", "beta-extra")) for branch in ref_branches 124 | ) 125 | 126 | if target_repo == "beta" and not all_branches_beta: 127 | self.errors.add("flat-manager-wrong-ref-branch-for-beta-repo") 128 | self.info.add( 129 | "flat-manager-wrong-ref-branch-for-beta-repo: If the target repo is 'beta' " 130 | + "then all refs must have branches ending with 'beta' or 'beta-extra'" 131 | ) 132 | 133 | if target_repo == "stable" and any_branches_beta: 134 | self.errors.add("flat-manager-wrong-ref-branch-for-stable-repo") 135 | self.info.add( 136 | "flat-manager-wrong-ref-branch-for-stable-repo: If the target repo is " 137 | + "'stable' then no ref must have branches ending " 138 | + "with 'beta' or 'beta-extra'" 139 | ) 140 | 141 | appref_list = [ref for ref in refs if ref.startswith("app/")] 142 | if not appref_list: 143 | return 144 | 145 | appref = appref_list[0] 146 | _, appid, _, branch = appref.split("/") 147 | 148 | if ( 149 | appid.endswith(config.FLATHUB_BASEAPP_IDENTIFIER) 150 | or appid.startswith("org.freedesktop.Platform.") 151 | or appid == "org.winehq.Wine" 152 | ): 153 | return 154 | 155 | if target_repo == "test" and branch in ("stable", "beta", "test"): 156 | return 157 | 158 | if branch != target_repo: 159 | self.errors.add("flat-manager-branch-repo-mismatch") 160 | -------------------------------------------------------------------------------- /tests/flatmanager.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | TOP_DIR="$(pwd)" 6 | ARCH="$(flatpak --default-arch)" 7 | TEST_REPO="tests/repo/min_success_metadata/gui-app" 8 | REQUIRED_COMMANDS="python gzip jq xmlstarlet git flatpak-builder flatpak ostree" 9 | [ "${GITHUB_ACTIONS:-false}" = "true" ] && REQUIRED_COMMANDS+=" dbus-run-session" 10 | FILES=( 11 | "docker/rewrite-manifest.py" 12 | "docker/flatpak-builder-lint-deps.json" 13 | "$TEST_REPO/org.flathub.gui.yaml" 14 | "tests/test_httpserver.py" 15 | ) 16 | 17 | check_command() { 18 | if ! command -v "$1" &> /dev/null; then 19 | echo "Error: Required command '$1' not found. Exiting." >&2 20 | exit 1 21 | fi 22 | } 23 | 24 | clean_up() { 25 | echo "Cleaning up: ${TEST_REPO}/{builddir,repo,.flatpak-builder}" 26 | rm -rf "${TEST_REPO}/builddir" "${TEST_REPO}/repo" "${TEST_REPO}/.flatpak-builder" 27 | echo "Cleaning up: build" 28 | rm -rf "build" 29 | echo "Cleaning up: .flatpak-builder" 30 | rm -rf ".flatpak-builder" 31 | } 32 | 33 | run_test() { 34 | local test_desc="$1" 35 | local expected_error="$2" 36 | 37 | export FLAT_MANAGER_BUILD_ID=0 FLAT_MANAGER_URL=http://localhost:9001 FLAT_MANAGER_TOKEN=foo 38 | local result 39 | result="$(flatpak run --command=flatpak-builder-lint org.flatpak.Builder//localtest --exceptions repo repo \ 40 | | jq -r '.errors|.[]' | xargs)" 41 | 42 | if [ -z "$expected_error" ]; then 43 | if [ -z "$result" ]; then 44 | echo "$test_desc: PASS ✅" 45 | return 0 46 | else 47 | echo "$test_desc: FAIL 🚨 Unexpected errors: $result" 48 | return 1 49 | fi 50 | else 51 | if [ "$result" = "$expected_error" ]; then 52 | echo "$test_desc: PASS ✅" 53 | return 0 54 | else 55 | echo "$test_desc: FAIL 🚨 Errors: $result" 56 | return 1 57 | fi 58 | fi 59 | } 60 | 61 | run_build() { 62 | if [ "${GITHUB_ACTIONS:-false}" = "true" ]; then 63 | dbus-run-session flatpak run org.flatpak.Builder//localtest \ 64 | --verbose --user --force-clean \ 65 | --state-dir="$GITHUB_WORKSPACE/cache/.flatpak-builder" --repo=repo \ 66 | --mirror-screenshots-url=https://dl.flathub.org/media \ 67 | --compose-url-policy=full \ 68 | --install-deps-from=flathub --ccache builddir \ 69 | org.flathub.gui.yaml 70 | else 71 | flatpak run org.flatpak.Builder//localtest --verbose --user \ 72 | --force-clean --repo=repo \ 73 | --mirror-screenshots-url=https://dl.flathub.org/media \ 74 | --compose-url-policy=full \ 75 | --install-deps-from=flathub --ccache builddir \ 76 | org.flathub.gui.yaml 77 | fi 78 | mkdir -p builddir/files/share/app-info/media 79 | ostree commit --repo=repo --canonical-permissions --branch=screenshots/"${ARCH}" builddir/files/share/app-info/media 80 | mkdir -p repo/appstream/x86_64 81 | cp -vf builddir/files/share/app-info/xmls/org.flathub.gui.xml.gz repo/appstream/x86_64/appstream.xml.gz 82 | if [ "$ARCH" != "x86_64" ]; then 83 | mkdir -p repo/appstream/"${ARCH}" 84 | cp -vf builddir/files/share/app-info/xmls/org.flathub.gui.xml.gz repo/appstream/"${ARCH}"/appstream.xml.gz 85 | fi 86 | } 87 | 88 | for cmd in $REQUIRED_COMMANDS; do 89 | check_command "$cmd" 90 | done 91 | 92 | if [ "$(git rev-parse --show-toplevel 2>/dev/null)" != "$TOP_DIR" ]; then 93 | echo "Error: Script must be run from the root of the git repository. Exiting." >&2 94 | exit 1 95 | fi 96 | 97 | for file in "${FILES[@]}"; do 98 | if [ ! -f "$file" ]; then 99 | echo "Error: Required file $file not found. Exiting." >&2 100 | exit 1 101 | fi 102 | done 103 | 104 | if [ -z "${GITHUB_ACTIONS:-}" ]; then 105 | echo "Setting up local Flatpak environment." 106 | flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 107 | 108 | if [ "${NO_CLEAN_UP:-0}" != "1" ]; then 109 | echo "Removing old Flatpak builder directory." 110 | rm -rf build 111 | fi 112 | 113 | if [ ! -d "build/org.flatpak.Builder" ]; then 114 | git clone --depth=1 --branch master --recursive --single-branch https://github.com/flathub/org.flatpak.Builder.git build/org.flatpak.Builder 115 | fi 116 | 117 | pushd build > /dev/null 118 | python3 ../docker/rewrite-manifest.py 119 | cd org.flatpak.Builder || exit 120 | cp -v ../../docker/flatpak-builder-lint-deps.json . 121 | flatpak-builder --user --force-clean --repo=repo --install-deps-from=flathub --default-branch=localtest --ccache --install builddir org.flatpak.Builder.json 122 | popd > /dev/null 123 | fi 124 | 125 | if ! flatpak info -r org.flatpak.Builder//localtest &> /dev/null; then 126 | echo "Error: Flatpak org.flatpak.Builder//localtest not installed. Exiting." >&2 127 | exit 1 128 | fi 129 | 130 | cd "$TOP_DIR" || exit 131 | rm -vf nohup.out server.pid 132 | nohup python tests/test_httpserver.py & 133 | sleep 5 134 | 135 | cd "$TEST_REPO" || true 136 | run_build 137 | 138 | run_test "Test 1" "appstream-no-flathub-manifest-key" || exit 1 139 | 140 | gzip -df repo/appstream/x86_64/appstream.xml.gz || true 141 | xmlstarlet ed --subnode "/components/component" --type elem -n custom \ 142 | --subnode "/components/component/custom" --type elem -n value -v \ 143 | "https://raw.githubusercontent.com/flathub-infra/flatpak-builder-lint/240fe03919ed087b24d941898cca21497de0fa49/tests/repo/min_success_metadata/gui-app/org.flathub.gui.yaml" \ 144 | repo/appstream/x86_64/appstream.xml | 145 | xmlstarlet ed --insert //custom/value --type attr -n key -v flathub::manifest > \ 146 | repo/appstream/x86_64/appstream-out-x86_64.xml 147 | mv repo/appstream/x86_64/appstream-out-x86_64.xml repo/appstream/x86_64/appstream.xml 148 | gzip repo/appstream/x86_64/appstream.xml || true 149 | 150 | if [ "$ARCH" != "x86_64" ]; then 151 | gzip -df repo/appstream/"${ARCH}"/appstream.xml.gz || true 152 | xmlstarlet ed --subnode "/components/component" --type elem -n custom \ 153 | --subnode "/components/component/custom" --type elem -n value -v \ 154 | "https://raw.githubusercontent.com/flathub-infra/flatpak-builder-lint/240fe03919ed087b24d941898cca21497de0fa49/tests/repo/min_success_metadata/gui-app/org.flathub.gui.yaml" \ 155 | repo/appstream/"${ARCH}"/appstream.xml | 156 | xmlstarlet ed --insert //custom/value --type attr -n key -v flathub::manifest > \ 157 | repo/appstream/"${ARCH}"/appstream-out-"${ARCH}".xml 158 | mv repo/appstream/"${ARCH}"/appstream-out-"${ARCH}".xml repo/appstream/"${ARCH}"/appstream.xml 159 | gzip repo/appstream/"${ARCH}"/appstream.xml || true 160 | fi 161 | 162 | run_test "Test 2" "" || exit 1 163 | 164 | cd "$TOP_DIR" || exit 165 | python tests/test_httpserver.py --stop 166 | if [ -z "${GITHUB_ACTIONS:-}" ]; then 167 | flatpak remove -y --noninteractive org.flatpak.Builder//localtest || true 168 | fi 169 | rm -vf nohup.out server.pid 170 | [ "${NO_CLEAN_UP:-0}" != "1" ] && clean_up 171 | 172 | unset FLAT_MANAGER_BUILD_ID FLAT_MANAGER_URL FLAT_MANAGER_TOKEN 173 | 174 | echo "All tests passed. 🎉" 175 | -------------------------------------------------------------------------------- /flatpak_builder_lint/checks/screenshots.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import tempfile 4 | 5 | from .. import appstream, builddir, config, ostree 6 | from . import Check 7 | 8 | 9 | def should_skip_mirror_check(has_test_ref: bool) -> bool: 10 | return has_test_ref and config.is_flathub_pipeline() 11 | 12 | 13 | class ScreenshotsCheck(Check): 14 | def _validate(self, path: str, appid: str, ref_type: str, has_test_ref: bool) -> None: 15 | appstream_path = f"{path}/app-info/xmls/{appid}.xml.gz" 16 | 17 | skip = False 18 | if appid.endswith(config.FLATHUB_BASEAPP_IDENTIFIER) or ref_type == "runtime": 19 | skip = True 20 | metainfo_dirs = [f"{path}/metainfo", f"{path}/appdata"] 21 | patterns = [ 22 | f"{appid}.metainfo.xml", 23 | f"{appid}.*.metainfo.xml", 24 | f"{appid}.appdata.xml", 25 | f"{appid}.*.appdata.xml", 26 | ] 27 | metainfo_files = [ 28 | os.path.abspath(file) 29 | for metainfo_dir in metainfo_dirs 30 | if os.path.isdir(metainfo_dir) 31 | for pattern in patterns 32 | for file in glob.glob(os.path.join(metainfo_dir, pattern)) 33 | if os.path.isfile(file) 34 | ] 35 | exact_metainfo = next( 36 | ( 37 | file 38 | for file in metainfo_files 39 | if file.endswith((f"{appid}.metainfo.xml", f"{appid}.appdata.xml")) 40 | ), 41 | None, 42 | ) 43 | 44 | if not skip and exact_metainfo is None: 45 | self.errors.add("appstream-metainfo-missing") 46 | self.info.add( 47 | f"appstream-metainfo-missing: No metainfo file for {appid} was found in" 48 | + " /app/share/metainfo or /app/share/appdata" 49 | ) 50 | return 51 | 52 | if ref_type != "app": 53 | return 54 | 55 | if exact_metainfo is not None: 56 | metainfo_sc = appstream.get_screenshot_images(exact_metainfo) 57 | metainfo_svg_sc_values = [i for i in metainfo_sc if i.endswith((".svg", ".svgz"))] 58 | 59 | metainfo_ctype = appstream.component_type(exact_metainfo) 60 | 61 | if metainfo_ctype in config.FLATHUB_APPSTREAM_TYPES_DESKTOP and not metainfo_sc: 62 | self.errors.add("metainfo-missing-screenshots") 63 | self.info.add( 64 | "metainfo-missing-screenshots: The metainfo file is missing screenshots" 65 | + " or it is not present under the screenshots/screenshot/image tag" 66 | ) 67 | return 68 | 69 | if metainfo_svg_sc_values: 70 | self.errors.add("metainfo-svg-screenshots") 71 | self.info.add("metainfo-svg-screenshots: The metainfo has a SVG screenshot") 72 | return 73 | 74 | if not skip and not os.path.exists(appstream_path): 75 | self.errors.add("appstream-missing-appinfo-file") 76 | self.info.add( 77 | "appstream-missing-appinfo-file: Appstream catalogue file is missing." 78 | + " Perhaps no Metainfo file was installed with correct name" 79 | ) 80 | return 81 | 82 | if os.path.exists(appstream_path): 83 | if len(appstream.components(appstream_path)) != 1: 84 | self.errors.add("appstream-multiple-components") 85 | return 86 | 87 | aps_ctype = appstream.component_type(appstream_path) 88 | 89 | sc_values = appstream.get_screenshot_images(appstream_path) 90 | 91 | sc_allowed_urls = (config.FLATHUB_MEDIA_BASE_URL,) 92 | 93 | if aps_ctype in config.FLATHUB_APPSTREAM_TYPES_DESKTOP and not sc_values: 94 | self.errors.add("appstream-missing-screenshots") 95 | self.info.add( 96 | "appstream-missing-screenshots: Catalogue file has no screenshots." 97 | + " Please check if screenshot URLs are reachable" 98 | ) 99 | return 100 | 101 | if ( 102 | not should_skip_mirror_check(has_test_ref) 103 | and sc_values 104 | and not any(s.startswith(sc_allowed_urls) for s in sc_values) 105 | ): 106 | self.errors.add("appstream-external-screenshot-url") 107 | self.info.add( 108 | "appstream-external-screenshot-url: Screenshots are not mirrored to" 109 | + f" {', '.join(sc_allowed_urls)}" 110 | ) 111 | return 112 | 113 | def check_build(self, path: str) -> None: 114 | ref_type, appid = builddir.infer_type(path), builddir.infer_appid(path) 115 | if not (appid and ref_type): 116 | return 117 | 118 | # ref branch is not exposed in builddir metadata 119 | self._validate(f"{path}/files/share", appid, ref_type, has_test_ref=False) 120 | 121 | def check_repo(self, path: str) -> None: 122 | self._populate_refs(path) 123 | refs = self.repo_primary_refs 124 | refs.update({r for r in ostree.get_refs(path, None) if r.startswith("screenshots/")}) 125 | app_refs = {ref for ref in refs if ref.startswith("app/") and len(ref.split("/")) == 4} 126 | if not app_refs: 127 | return 128 | 129 | has_test_ref = False 130 | for ref in app_refs: 131 | appid = ref.split("/")[1] 132 | arch = ref.split("/")[2] 133 | branch = ref.split("/")[3] 134 | 135 | if branch == "test": 136 | has_test_ref = True 137 | 138 | with tempfile.TemporaryDirectory() as tmpdir: 139 | for subdir in ("appdata", "metainfo", "app-info"): 140 | os.makedirs(os.path.join(tmpdir, subdir), exist_ok=True) 141 | ostree.extract_subpath( 142 | path, ref, f"files/share/{subdir}", os.path.join(tmpdir, subdir), True 143 | ) 144 | 145 | self._validate(tmpdir, appid, "app", has_test_ref) 146 | appstream_path = f"{tmpdir}/app-info/xmls/{appid}.xml.gz" 147 | 148 | if not should_skip_mirror_check(has_test_ref) and os.path.exists(appstream_path): 149 | aps_ctype = appstream.component_type(appstream_path) 150 | 151 | if aps_ctype in config.FLATHUB_APPSTREAM_TYPES_DESKTOP: 152 | if f"screenshots/{arch}" not in refs: 153 | self.errors.add("appstream-screenshots-not-mirrored-in-ostree") 154 | return 155 | 156 | media_path = os.path.join(tmpdir, "app-info", f"screenshots-{arch}") 157 | media_glob_path = f"{media_path}/**" 158 | ostree.extract_subpath(path, f"screenshots/{arch}", "/", media_path) 159 | 160 | ref_sc_files = { 161 | os.path.basename(path) 162 | for path in glob.glob(media_glob_path, recursive=True) 163 | if path.endswith(".png") 164 | } 165 | 166 | if not ref_sc_files: 167 | self.errors.add("appstream-screenshots-files-not-found-in-ostree") 168 | --------------------------------------------------------------------------------