arrow-left

All pages
gitbookPowered by GitBook
1 of 7

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Our First App

Launch-able, Build-able, and Shareable

In the previous chapter, we created a "Hello World!" app to show off our Vala and Gtk skills. But what if we wanted to share our new app with a friend? They'd have to know which packages to include with the valac command we used to build our app, and after they'd built it they'd have to run it from the build directory like we did. Clearly, we need to do some more stuff to make our app fit for people to use, to make it a real app.

hashtag
Hello (Again) World!

To create our first real app, we're going to re-use what we learned in the last chapter and build on that example.

  1. Create a new project folder, including a new "src" folder and an Application.vala file

  2. Create a Gtk.Application class with a Gtk.ApplicationWindow in the activate () function.

The results should look like this:

Build "Application.vala" just to make sure it all works. If something goes wrong here, feel free to refer back to and remember to check your terminal output for any hints.

Initialize a git branch, add your files to the project, and write a commit message using what you learned in the last chapter. Lastly, make sure you've created a new repository for your project on GitHub push your first revision with git:

Everything working as expected? Good. Now, let's get our app ready for other people to use.

hashtag
License

Since we're going to be putting our app out into the wild, we should include some information about who wrote it and the legal usage of its source code. For this we need a new file in our project's root folder: the LICENSE file. This file contains a copy of the license that your code is released under. For elementary OS apps this is typically the (GPL). Remember the header we added to our source code? That header reminds people that your app is licensed and it belongs to you. GitHub has a built-in way to add several popular licenses to your repository. Read their and add a LICENSE file to your repository.

circle-info

If you'd like to better understand software licensing, the Linux Foundation offers a

Next, we need a way to create a listing in AppCenter and a way to tell the interface to show our app in the Applications menu and dock: let's write some Metadata.

Add a new Gtk.Label to the window with the text "Hello Again World!"
  • Finally, this time we're going to prefix our file with a small legal header. Make sure this header matches the license of your code and assigns copyright to you. More info about this later.

  • the last chapter
    GNU General Public Licensearrow-up-right
    documentation for adding software licensesarrow-up-right
    free online course on open source licensingarrow-up-right
    src/Application.vala
    /*
     * SPDX-License-Identifier: GPL-3.0-or-later
     * SPDX-FileCopyrightText: 2023 Your Name <[email protected]>
     */
     
     public class MyApp : Gtk.Application {
        public MyApp () {
            Object (
                application_id: "io.github.yourusername.yourrepositoryname",
                flags: ApplicationFlags.DEFAULT_FLAGS
            );
        }
    
        protected override void activate () {
            var label = new Gtk.Label ("Hello Again World!");
       
            var main_window = new Gtk.ApplicationWindow (this) {
                child = label,
                default_height = 300,
                default_width = 300,
                title = "Hello World"
            };
            main_window.present ();
        }
    
        public static int main (string[] args) {
            return new MyApp ().run (args);
        }
    }
    git remote add origin [email protected]:yourusername/yourrepositoryname.git
    git push -u origin master

    The Build System

    Compiling Binaries & Installing Data with Meson

    The next thing we need is a build system. The build system that we're going to be using is called Mesonarrow-up-right. We already installed the meson program at the beginning of this book when we installed elementary-sdk. What we're going to do in this step is create the files that tell Meson how to install your program. This includes all the rules for building your source code as well as correctly installing your icons, .desktop, and metainfo files and the binary app that results from the build process.

    Create a new file in your project's root folder called "meson.build". We've included some comments along the way to explain what each section does. You don't have to copy those, but type the rest into that file:

    Notice that in each of our install_data methods, we rename our files using our project name. By using our project name—and its RDNN scheme—we will ensure that our files are installed under a unique name that won't cause conflicts with other apps.

    And you're done! Your app now has a real build system. This is a major milestone in your app's development!

    circle-info

    Don't forget to add these files to git and push to GitHub.

    hashtag
    Building and Installing with Meson

    Now that we have a build system, let's try it out. Configure the build directory using the Meson command in Terminal:

    This command tells Meson to get ready to build our app using the prefix "/usr" and that we want to build our app in a clean directory called "build". The meson setup command defaults to installing our app locally, but we want to install our app for all users on the computer.

    Change into the build directory and use ninja to build. Then, if the build is successful, install with ninja install:

    If all went well, you should now be able to open your app from the Applications Menu and pin it to the Dock. We'll revisit Meson again later to add some more complicated behavior, but for now this is all you need to know to give your app a proper build system. If you want to explore Meson a little more on your own, you can always check out .

    circle-exclamation

    If you were about to add the "build" folder to your git repository and push it, stop! This binary was built for your computer and we don't want to redistribute it. In fact, we built your app in a separate folder like this so that we can easily delete or ignore the "build" folder and it won't mess up our app's source code.

    hashtag
    Uninstalling the application

    To uninstall your application, change into the build directory and we will use ninja once again to uninstall:

    If all went well, you should see command output that shows files related to your application were removed. Again, more details can be found in .

    hashtag
    Review

    Let's review all we've learned to do:

    • Create a new Gtk app using Gtk.Window, Gtk.Button, and Gtk.Label

    • Keep our projects organized into branches

    That's a lot! You're well on your way to becoming a bona fide app developer for elementary OS. Give yourself a pat on the back, then take some time to play around with this example. Change the names of files and see if you can still build and install them properly. Ask another developer to clone your repo from GitHub and see if it builds and installs cleanly on their computer. If so, you've just distributed your first app! When you're ready, we'll move onto the next section: Translations.

    circle-info

    If you're having trouble, you can view the full example code

    # project name, programming language, and required meson version
    project('io.github.yourusername.yourrepositoryname',
        'vala', 'c',
        meson_version: '>= 0.56.0'
    )
    
    # Create a new executable, list the files we want to compile, list the dependencies we need, and install
    executable(
        meson.project_name(),
        'src' / 'Application.vala',
        dependencies: [
            dependency('gtk4')
        ],
        install: true
    )
    
    # Install our .desktop file so the Applications Menu will see it
    install_data(
        'data' / 'hello-again.desktop',
        install_dir: get_option('datadir') / 'applications',
        rename: meson.project_name() + '.desktop'
    )
    
    # Install our .metainfo.xml file so AppCenter will see it
    install_data(
        'data' / 'hello-again.metainfo.xml',
        install_dir: get_option('datadir') / 'metainfo',
        rename: meson.project_name() + '.metainfo.xml'
    )
    License our app under the GPL and declare our app's authors in a standardized manner
  • Create a .desktop file using RDNN that tells the computer how to display our app in the Applications Menu and the Dock

  • Set up a Meson build system that contains all the rules for building our app and installing it cleanly

  • Uninstall our application cleanly

  • Meson's documentationarrow-up-right
    Meson's documentationarrow-up-right
    here on GitHubarrow-up-right
    meson setup build --prefix=/usr
    cd build
    ninja
    ninja install
    sudo ninja uninstall

    Metadata

    List your app in AppCenter and make it accessible from the Applications menu and Dock.

    Every app comes with two metadata files that we can generate using an online tool. Open AppStream Metainfo Creatorarrow-up-right and fill out the form with your app's info:

    circle-info

    When you get the section titled "Launchables", make sure to select "Generate a .desktop file for me".

    Don't worry about generating Meson snippets, as we'll cover that in the next section. After you select "Generate", you should have two resulting files that you can copy.

    hashtag
    MetaInfo

    First is a MetaInfo file. This file contains all the information needed to list your app in AppCenter. It should look something like this:

    In your project's root, create a new folder called "data", and save your MetaInfo to a new file called "hello-again.metainfo.xml".

    circle-info

    Below are some of the most important fields for publishing in AppCenter, but there are even more that you can read about

    hashtag
    Screenshots

    For the purposes of this tutorial, screenshots are optional, but they are required for publishing in AppCenter. Screenshots should only show your app on a transparent background and not contain any additional text or illustrations. You can use the caption tag to provide translatable and accessible descriptions of your screenshots.

    circle-info

    You can use the built-in Screenshot app on elementary OS with the "grab the current window" option or secondary-click on your app's title area and select "Take Screenshot" to get transparent window-only screenshots of your app

    hashtag
    Content Warnings

    We use the standard to describe sensitive content that may be present in your app so that people using it can be informed and actively consent to seeing that content. OARS data is required and can be generated by taking :

    hashtag
    Branding

    You can also specify a brand color for your app by adding the branding tag inside the component tag. Colors must be in hexadecimal, starting with #. The background will automatically be given a slight gradient in your app's banner.

    hashtag
    Monetization

    If you want to monetize your app, you will need to add two keys inside a custom tag inside the component tag. Suggested prices should be in whole USD. You also must include your app's AppCenter Stripe key. This is a unique public key for each app and is not the same as your Stripe account's regular public key. You can connect your app to Stripe and receive a new key on the .

    circle-info

    Remember that AppCenter is a pay-what-you-want store. A suggested price is not a price floor. Users will still be able to choose any price they like, including 0.

    hashtag
    Releases

    Your app must have a release tag for every version you wish to publish in AppCenter, including the initial release.

    circle-info

    For more details on available features and advice on writing engaging release notes see .

    hashtag
    Desktop Entry

    This file contains all the information needed to display your app in the Applications Menu and in the Dock. The one generated from AppStream Metainfo Creator looks something like this:

    Copy the contents of your Desktop Entry and save it to the data folder you created earlier. Name this new file "hello-again.desktop".

    circle-info

    For more info about crafting .desktop files, check out .

    hashtag
    Mark Your Progress

    Each time we add a new file or make a significant change it's a good idea to commit a new revision and push to GitHub. Keep in mind that this acts as a backup system as well; when we push our work to GitHub, we know it's safe and we can always revert to a known good revision if we mess up later.

    Add all of these files to git and commit a revision:

    Now that we've got all these swanky files laying around, we need a way to tell the computer what to do with them. Ready for the next chapter? Let's do this!

    optional fieldsarrow-up-right
    Open Age Rating Service (OARS)arrow-up-right
    a short surveyarrow-up-right
    AppCenter Dashboardarrow-up-right
    Publishing Updates
    this HIG entryarrow-up-right
    <?xml version="1.0" encoding="UTF-8"?>
    <component type="desktop-application">
      <id>io.github.myteam.myapp</id>
    
      <name>My App</name>
      <summary>Proves that we can use Vala and Gtk</summary>
    
      <metadata_license>CC-BY-4.0</metadata_license>
      <project_license>GPL-3.0-or-later</project_license>
    
      <description>
        <p>
          A quick summary of your app's main selling points and features. Just a couple sentences per paragraph is best
        </p>
      </description>
    
      <launchable type="desktop-id">io.github.myteam.myapp.desktop</launchable>
    </component>
    <screenshots>
      <screenshot type="default">
        <caption>The most important feature of my app</caption>
        <image>https://raw.githubusercontent.com/myteam/myapp/1.0.0/data/screenshot.png</image>
      </screenshot>
    </screenshots>
    <branding>
      <color type="primary">#f37329</color>
    </branding>
    <custom>
      <value key="x-appcenter-suggested-price">5</value>
      <value key="x-appcenter-stripe">pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</value>
    </custom>
    <releases>
      <release version="1.0.0" date="2022-09-08">
        <description>
          <p>Initial release, includes features such as:</p>
          <ul>
            <li>Application can be launched</li>
          </ul>
        </description>
      </release>
    </releases>
    [Desktop Entry]
    Version=1.0
    Type=Application
    
    Name=My App
    Comment=Proves that we can use Vala and Gtk
    Categories=Development;Education;
    
    Icon=io.github.myteam.myapp
    Exec=io.github.myteam.myapp
    Terminal=false
    git add data/
    git commit -am "Add Metadata files"
    git push

    Packaging

    While having a build system is great, our app still isn't ready for regular users. We want to make sure our app can be built and installed without having to use Terminal. What we need to do is package our app. To do this, we use Flatpak on elementary OS. This section will teach you how to package your app as a Flatpak, which is required to publish apps in AppCenter. This will allow people to install your app and even get updates for it when you publish them.

    hashtag
    Practice Makes Perfect

    If you want to get really good really fast, you're going to want to practice. Repetition is the best way to commit something to memory. So let's recreate our entire Hello World app again from scratch:

    1. Create a new branch folder "hello-packaging"

    2. Set up our directory structure including the "src" and "data" folders.

    3. Add your LICENSE, .desktop, .metainfo.xml, icons, and source code.

    4. Now set up the Meson build system and translations.

    5. Test everything!

    Did you commit and push to GitHub for each step? Keep up these good habits and let's get to packaging this app!

    hashtag
    Flatpak Manifest

    The Flatpak manifest file describes your app's build dependencies and required permissions. Create a io.github.yourusername.yourrepositoryname.yml file in your project root with the following contents:

    To run a test build and install your app, we can use flatpak-builder with a few arguments:

    This tells Flatpak Builder to build the manifest we just wrote into a clean build folder the same as we did for Meson. Plus, we install the built Flatpak package locally for our user. If all goes well, congrats! You've just built and installed your app as a Flatpak.

    That wasn't too bad, right? We'll set up more complicated packaging in the future, but this is all that is required to submit your app to AppCenter Dashboard for it to be built, packaged, and distributed. If you'd like you can always read .

    hashtag
    Uninstalling the application

    Ninja and Flatpak both provide commands to uninstalling the application. It is recommended to use the provided method in both cases.

    To remove our application with Flatpak, we will use Flatpak's remove command:

    Flatpak will prompt you to remove your application.

    Note: You can append -y to the command to bypass the dialog confirmation prompt

    If you'd like you can always read .

    Translations

    Now that you've learned about Meson, the next step is to make your app able to be translated to different languages. The first thing you need to know is how to mark strings in your code as translatable. Here's an example:

    See the difference? We marked the string as translatable by adding _() around it. Go back to your project and make all your strings translatable by adding _().

    Now we have to make some changes to our Meson build system and add a couple new files to describe which files we want to translate and which languages we want to translate into.

    more about Flatpakarrow-up-right
    more about Flatpak removearrow-up-right
    Open up your "meson.build" file and add these lines below your project declaration:
  • Remove the lines that install your .desktop and metainfo files and replace them with the following:

    The merge_file method combines translating and installing files, similarly to how the executable method combines building and installing your app. You might have noticed that this method has both an input argument and an output argument. We can use this instead of the rename argument from the install_data method.

  • Still in this file, add the following as the last line:

  • You might have noticed in step 2 that the merge_file method has an input and output. We're going to append the additional extension .in to our .desktop and .metainfo.xml files so that this method can take the untranslated files and produce translated files with the correct names.

    We use the git mv command here instead of renaming in the file manager or with mv so that git can keep track of the file rename as part of our revision history.

  • Now, Create a directory named "po" in the root folder of your project. Inside of your po directory you will need to create another "meson.build" file. This time, its contents will be:

  • Inside of "po" create another file called "POTFILES" that will contain paths to all of the files you want to translate. For us, this looks like:

  • Now it's time to go back to your build directory and generate some new files using Terminal! The first one is our translation template or .pot file:

    After running this command you should notice a new file in the po directory containing all of the translatable strings for your app.

  • We have one more file to create in the "po" directory. This file will be named "LINGUAS" and it should contain the two-letter language codes for all languages you want to provide translations for. As an example, let's add German and Spanish

  • Now we can use the template file to generate translation files for each of the languages we listed in the LINGUAS file with the following command:

    You should notice two new files in your po directory called de.po and es.po. These files are now ready for translators to localize your app!

  • Last step. Don't forget to add all of the new files we created in the po directory to git:

  • That's it! Your app is now fully ready to be translated. Remember that each time you add new translatable strings or change old ones, you should regenerate your .pot and po files using the -pot and -update-po build targets from the previous two steps. If you want to support more languages, just list them in the LINGUAS file and generate the new po file with the -update-potarget. Don't forget to add any new po files to git!

    circle-info

    If you're having trouble, you can view the full example code here on GitHubarrow-up-right

    hashtag
    Translators Comments

    Sometimes detailed descriptions in the context of translatable strings are necessary for disambiguation or to help in the creation of accurate translations. For these situations use /// TRANSLATORS: comments.

    # This is the same ID that you've used in meson.build and other files
    id: io.github.yourusername.yourrepositoryname
    
    # Instead of manually specifying a long list of build and runtime dependencies,
    # we can use a convenient pre-made runtime and SDK. For this example, we'll be
    # using the runtime and SDK provided by elementary.
    runtime: io.elementary.Platform
    runtime-version: '8'
    sdk: io.elementary.Sdk
    
    # This should match the exec line in your .desktop file and usually is the same
    # as your app ID
    command: io.github.yourusername.yourrepositoryname
    
    # Here we can specify the kinds of permissions our app needs to run. Since we're
    # not using hardware like webcams, making sound, or reading external files, we
    # only need permission to draw our app on screen using either X11 or Wayland.
    finish-args:
      - '--share=ipc'
      - '--socket=fallback-x11'
      - '--socket=wayland'
    
    # This section is where you list all the source code required to build your app.
    # If we had external dependencies that weren't included in our SDK, we would list
    # them here.
    modules:
      - name: yourrepositoryname
        buildsystem: meson
        sources:
          - type: dir
            path: .
    flatpak-builder build io.github.yourusername.yourrepositoryname.yml --user --install --force-clean
    flatpak remove io.github.yourusername.yourrepositoryname
    flatpak uninstall io.github.yourusername.yourrepositoryname --delete-data
    
    
            ID                                                          Branch           Op
     1.     io.github.yourusername.yourrepositoryname                   master           r
     2.     io.github.yourusername.yourrepositoryname.Locale            master           r
    
    Proceed with these changes to the user installation? [Y/n]:
    #Translate and install our .desktop file
    i18n.merge_file(
        input: 'data' / 'hello-again.desktop.in',
        output: meson.project_name() + '.desktop',
        po_dir: meson.project_source_root() / 'po',
        type: 'desktop',
        install: true,
        install_dir: get_option('datadir') / 'applications'
    )
    
    #Translate and install our .metainfo file
    i18n.merge_file(
        input: 'data' / 'hello-again.metainfo.xml.in',
        output: meson.project_name() + '.metainfo.xml',
        po_dir: meson.project_source_root() / 'po',
        install: true,
        install_dir: get_option('datadir') / 'metainfo'
    )
    subdir('po')
    git mv data/hello-again.desktop data/hello-again.desktop.in
    git mv data/hello-again.metainfo.xml data/hello-again.metainfo.xml.in
    i18n.gettext(meson.project_name(),
        args: '--directory=' + meson.project_source_root(),
        preset: 'glib'
    )
    src/Application.vala
    data/hello-again.desktop.in
    data/hello-again.metainfo.xml.in
    ninja io.github.yourusername.yourrepositoryname-pot
    de
    es
    ninja io.github.yourusername.yourrepositoryname-update-po
    git add src/Application.vala meson.build po/ data/
    git commit -am "Add translations"
    git push
    stdout.printf ("Not Translatable string");
    stdout.printf (_("Translatable string!"));
    
    string normal = "Another non-translatable string";
    string translated = _("Another translatable string");
    # Include the translations module
    i18n = import('i18n')
    
    # Set our translation domain
    add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c')
    /// TRANSLATORS: The first %s is search term, the second is the name of default browser
    title = _("Search for %s in %s").printf (query, get_default_browser_name ());

    Icons

    The last thing we need for a minimum-viable-app is to provide app icons. Apps on elementary OS provide icons hinted in 6 sizes: 16px, 24px, 32px, 48px, 64px, and 128px. These icons will be shown in many places throughout the system, such as those listed below:

    Size
    AppCenter
    Applications Menu
    Dock
    Menus & Popovers
    Multitasking
    Notifications

    Continuous Integration

    Continuous integration testing (also called CI), is a way to automatically ensure that your app builds correctly and has the minimum necessary configuration to run when it's installed. Setting up CI for your app can save you time and give you reassurance that when you submit your app for publishing it will pass the automated testing phase in AppCenter Dashboard. Keep in mind however, that there is also a human review portion of the submission process, so an app that passes CI may still need fixing before being published in AppCenter. Now that you have an app with a build system, metadata files, and packaging, we can configure CI using GitHub Actions.

    1. Navigate to your project's page on GitHub and select the "Actions" tab.

    2. Select "set up a workflow yourself". You'll then be shown the

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✖️ No

    24px

    ✖️ No

    ✖️ No

    ✖️ No

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    32px

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✖️ No

    48px

    ✔️ Yes

    ✖️ No

    ✔️ Yes

    ✖️ No

    ✔️ Yes

    ✔️ Yes

    64px

    ✔️ Yes

    ✔️ Yes

    ✔️ Yes

    ✖️ No

    ✔️ Yes

    ✖️ No

    128px

    ✔️ Yes

    ✖️ No

    ✖️ No

    ✖️ No

    ✖️ No

    ✖️ No

    circle-info

    To help you provide the necessary sizes—and for this tutorial—Micah Ilbery maintains an icon template project here on GitHubarrow-up-right

    Place your icons in the data directory and name them after their pixel sizes, such as32.svg, 64.svg, etc. The file structure should look like this:

    Now that you have icon files in the data directory, add the following lines to the end of meson.build to install them .

    You'll notice the section for installing app icons is a little more complicated than installing other files. In this example, we're providing SVG icons in all of the required sizes for AppCenter and, since we're using SVG, we're installing them for both LoDPI and HiDPI. If you're providing PNG icons instead, you'll need to tweak this part a bit to handle assets exported for use on HiDPI displays.

    If you cannot see your new icon in the Applications Menu or the Dock once you've reinstalled your app, refresh your system's icon cache using the following command:

    circle-info

    For more information about creating and hinting icons, check out the Human Interface Guidelinesarrow-up-right

    16px

    ✖️ No

    main.yml
    file with GitHub's default CI configuration. Replace the default workflow with the following:
    1. Select "Start commit" in the top right corner of the page to commit this workflow to your repository.

    That's it! The Flatpak CI workflow will now run on your repository for any new commits or pull requests. You'll be able to see if the build succeeded or failed, and any reasons it might have failed. Configuring this CI workflow will help guide you during the development process and ensure that everything is working as intended.

    This workflow will also produce a Flatpak Bundle file using GitHub Artifacts that you can download and install with Sideload. Using this bundle file, you can test feature or bug fix branches with your team, contributors, or community before merging the proposed fix into your main git branch.

    GitHub Actions can be used to configure many more types of CI or other automation. For more information, check out the GitHub Actions websitearrow-up-right.

    hello-again
        data
            16.svg
            24.svg
            32.svg
            48.svg
            64.svg
            128.svg
    # Install our icons in all the required sizes
    icon_sizes = ['16', '24', '32', '48', '64', '128']
    
    foreach i : icon_sizes
        install_data(
            'data' / i + '.svg',
            install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps',
            rename: meson.project_name() + '.svg'
        )
        install_data(
            'data' / i + '.svg',
            install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps',
            rename: meson.project_name() + '.svg'
        )
    endforeach
    sudo update-icon-caches /usr/share/icons/*
    name: CI
    
    # This workflow will run for any pull request or pushed commit
    on: [push, pull_request]
    
    # A workflow run is made up of one or more jobs that can run sequentially or in parallel
    jobs:
      # This workflow contains a single job called "flatpak"
      flatpak:
        # The type of runner that the job will run on
        runs-on: ubuntu-latest
    
        # This job runs in a special container designed for building Flatpaks for AppCenter
        container:
          image: ghcr.io/elementary/flatpak-platform/runtime:8
          options: --privileged
    
        # Steps represent a sequence of tasks that will be executed as part of the job
        steps:
          # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it
        - uses: actions/checkout@v3
    
          # Builds your flatpak manifest using the Flatpak Builder action
        - uses: flatpak/flatpak-github-actions/flatpak-builder@v6
          with:
            # This is the name of the Bundle file we're building and can be anything you like
            bundle: MyApp.flatpak
            # This uses your app's RDNN ID
            manifest-path: io.github.yourusername.yourrepositoryname.yml
    
            # You can automatically run any of the tests you've created as part of this workflow
            run-tests: true
    
            # These lines specify the location of the elementary Runtime and Sdk
            repository-name: appcenter
            repository-url: https://flatpak.elementary.io/repo.flatpakrepo
            cache-key: "flatpak-builder-${{ github.sha }}"
    OARS: Open Age Ratings Servicehughsie.github.iochevron-right
    AppStream MetaInfo Creatorwww.freedesktop.orgchevron-right
    Logo
    Logo