Commit b9f2e028 authored by David Boddie's avatar David Boddie

Merge branch 'resources-tutorial' into 'master'

Add a tutorial to show how to use resource bundles

See merge request Librem5/developer.puri.sm!203
parents 0aaabbb3 153f9ea4
Pipeline #4238 passed with stages
in 9 minutes and 56 seconds
.. _App_Resources_tutorial_building:
Building the Application
========================
.. contents::
:local:
The ``app`` directory and its subdirectories contain ``meson.build`` files that
describe how the application is built. These are used by the `Meson`_ build
tool to configure the build process.
Meson is usually run so that it creates a build directory. This is where all
the resources are put so that the `Ninja`_ build tool can perform the actual
process of building and installing the application.
Top-Level Build File
--------------------
In the ``app`` directory itself, the ``meson.build`` file begins with a
declaration of the project name, version and build system requirements:
.. literalinclude:: app/meson.build
We also declare the ``data`` and ``src`` subdirectories, causing Meson to
examine them for any ``meson.build`` files they may contain.
The last line causes a special script to be run after installation. It is not
important to know what this is doing at this point.
Sources Build File
------------------
The ``meson.build`` file in the ``src`` directory describes how the source
files are processed when the build occurs:
.. literalinclude:: app/src/meson.build
In this case, we instruct Meson to take the ``main.py`` file in the ``src``
directory and copy it into the build directory as ``app-resources`` --
this is the name given as the executable in the :ref:`desktop entry file
<App_Resources_tutorial_desktop_file>`.
We also declare that the file should be installed, and that its installation
directory is the system location for executables (``bindir``).
Data Build File
---------------
The ``meson.build`` file in the ``data`` describes how the data files are
processed when the build occurs:
.. literalinclude:: app/data/meson.build
Here, we tell Meson to simply copy the ``com.example.app_resources.desktop``
file into the build directory. We also declare that it should be installed, and
that its installation directory is the ``applications`` subdirectory of the
system location for data files (``datadir``).
The ``com.example.app_resources.svg`` file is more easily described to
Meson. It will be installed in the appropriate subdirectory of the system
location for data files that is used for icons.
Building using Meson and Ninja
------------------------------
When building the application for deployment on the phone, we will use Flatpak
to coordinate the build process. However, behind the scenes, we are using Meson
and Ninja to perform the actual configuration and build. If you want to try and
build the application for testing on your workstation, you can follow the steps
below to build, install, and finally uninstall it.
To configure the build on the command line, enter the ``app`` directory and
run Meson, specifying the source and build directories::
meson . _build
Build the application using Ninja, passing the build directory as an argument
so that the build occurs within that directory. There is no need to specify a
build rule because the default rule builds the application::
ninja -C _build
Finally, use ``sudo`` to install the application in a standard location on your
system using the ``install`` build rule::
sudo ninja -C _build install
To uninstall the application, run its ``uninstall`` rule::
sudo ninja -C _build uninstall
All of the files that were installed should now have been cleanly removed from
system locations.
Summary
-------
We have examined the contents of some ``meson.build`` files to see how simple
rules are used to describe the configuration process. We have also seen how
Meson and Ninja are used to configure and build the application.
The application can also be packaged for more convenient installation using the
Flatpak framework. This is the subject of the next part of the tutorial.
.. include:: /links.txt
.. _App_Resources_tutorial_data:
Data Files
==========
.. contents::
:local:
Inside the ``data`` directory are three files: ``com.example.app_resources.desktop``,
``com.example.app_resources.svg`` and ``meson.build``.
The ``com.example.app_resources.desktop`` file is a `Desktop Entry`_ file.
The ``com.example.app_resources.svg`` file is a `Scalable Vector Graphics`_
(SVG) file that contains the application icon.
The ``meson.build`` file describes the build rules for the program -- we will
look at these later in :ref:`App_Resources_tutorial_building`.
.. _App_Resources_tutorial_desktop_file:
The Desktop Entry File
----------------------
The ``com.example.app_resources.desktop`` contains metadata about the
application that browsers and application launchers can use to show information
about the application, including its name, icon and the name of its executable
file. The contents of the file itself follows a simple format with many entries
being self-documenting:
.. literalinclude:: app/data/com.example.app_resources.desktop
Less obvious entries are ``Exec`` and ``Terminal``. The ``Exec`` entry holds the
file name of the executable used to run the application. This will be defined
in the build script we use. The ``Terminal`` entry determines whether the
application should be run in a terminal program. We can ignore the other
entries for now.
The Icon File
-------------
The ``com.example.app_resources.svg`` file is simply an SVG file:
.. image:: app/data/com.example.app_resources.svg
We use an SVG file for convenience -- it can be rescaled as needed by the user
interface that shows it because the icon is stored in a vector file format. As
a result, we do not need to provide bitmaps of different sizes.
Summary
-------
In this part of the tutorial we looked at two files that provide information
about the application (a desktop entry file) and a visual representation for it
(an SVG icon file).
Next, we will see how the main program and the data files are put together when
the application is built.
.. include:: /links.txt
Getting the Application
=======================
The application can be found in the `Librem 5 developer documentation
repository`_. You can clone this repository locally with Git::
git clone https://source.puri.sm/Librem5/developer.puri.sm.git
This should result in the creation of a directory called ``developer.puri.sm``
in your current directory. Enter this directory and find the application
directory for this tutorial::
cd developer.puri.sm/Apps/Tutorials/App_Resources/app
You can build the application from the command line or open it in :ref:`gbuilder`.
.. include:: /links.txt
Overview of Files and Directories
=================================
The main ``app`` directory contains the following files and directories. Those
marked with a trailing slash are directories.
====================================== =========================================
Name Description
====================================== =========================================
**build-aux/** Helper scripts for the build system.
**com.example.app_resources.json** Manifest file for deployment.
**data/** Data files for the installed application.
**meson.build** The main build file.
**src/** Source files for the application.
====================================== =========================================
The first four of these are concerned with building and packaging the
application, or contain data files that are not part of the application itself.
Sources and Resources
---------------------
The ``src`` directory is where we will start to explore how the application is
put together. It contains a collection of files and an ``images`` directory.
====================================== =========================================
Name Description
====================================== =========================================
**app_resources.gresource.xml** Resource description file.
**app-resources.in** Template executable file.
**images/** Images that will be stored in the
resource bundle.
**__init__.py** Python package file.
**main.py** The main module for the application.
**meson.build** The build file for this directory.
====================================== =========================================
The ``meson.build`` file describes the build rules for the program -- we will
look at these later in :ref:`App_Resources_tutorial_building`.
Next, we will look at the ``app_resources.gresource.xml`` file and how
resources are described.
.. _App_Resources_tutorial_packaging:
Packaging the Application
=========================
.. contents::
:local:
The ``app`` directory contains a ``com.example.app_resources.json``
manifest file for use with Flatpak. This describes where the application
source code can be obtained from, how the application is built, the
dependencies that need to be built with it, and the permissions it needs when
it is run.
Writing the Manifest
--------------------
The manifest for this simple application is short, so we include the whole file
here to provide an overview before looking at the details:
.. literalinclude:: app/com.example.app_resources.json
We examine the three main parts of the manifest individually.
Application Information
~~~~~~~~~~~~~~~~~~~~~~~
In the manifest we record information about the application, including its ID
and the command used to run it:
.. literalinclude:: app/com.example.app_resources.json
:start-at: {
:end-at: command
The ``runtime`` and ``runtime-version`` values define precisely which
collection of libraries the application needs to run. The corresponding ``sdk``
value tells Flatpak which SDK is needed to build the application. When building
applications for a GNOME-based platform the corresponding SDK is required.
Permissions
~~~~~~~~~~~
The ``finish-args`` list is typically used to state which permissions the
application needs. In this case it will need access to the Wayland display
server to be able to show a window:
.. literalinclude:: app/com.example.app_resources.json
:start-at: finish-args
:end-at: ],
Other permissions will be required if you want to store and modify settings for
your application, access peripherals, and perform other tasks that require the
user's consent.
Modules
~~~~~~~
The ``modules`` list describes the application components (or modules),
including any dependencies that need to be bundled with it, in a list of
dictionaries:
.. literalinclude:: app/com.example.app_resources.json
:start-at: modules
:end-at: builddir
The ``name`` of the application should be unique in the list of modules but can
be otherwise freely assigned. We indicate that we are using the Meson build
system and instruct Flatpak to use a separate build directory.
After this, we describe where the sources of the application should be obtained
from. For many applications we would describe the location of a repository.
However, to make things simple, we indicate that the sources are in the same
directory as the manifest:
.. literalinclude:: app/com.example.app_resources.json
:start-at: modules
This concludes the description of the main (and only) module.
Building the Application
------------------------
Following the instructions in :ref:`cross_building_flatpaks_app`, we use
``flatpak-builder`` to build a flatpak for the application. To do this we need
to install the runtime and SDK that the application depends on. These are the
same as those mentioned in :ref:`flatpak_setup_gnome` except that we need the
ones for the aarch64 architecture::
flatpak --user install flathub org.gnome.Platform/aarch64/3.30 org.gnome.Sdk/aarch64/3.30
After installing these, build the application with `flatpak-builder`::
flatpak-builder --arch=aarch64 --repo=myrepo _flatpak com.example.app_resources.json
The result is stored in the ``myrepo`` directory, which is a local repository.
It can be exported from the repository as a binary bundle for deployment on
the target device. We do this by running ``flatpak`` with the ``build-bundle``
command, passing the repository, the name of the bundle to create and the
application ID::
flatpak build-bundle --arch=aarch64 myrepo app.flatpak com.example.app_resources
In this case the bundle is written to the ``app.flatpak`` file. This can be
copied to the phone or development board for installation.
Installing the Application
--------------------------
One way to install the application is to use the ``flatpak`` tool on the
command line to install the binary bundle. Other more user-friendly ways are
also possible.
First of all, copy the bundle to the target device and make it readable by the
``purism`` user. Then, log in to the device and ensure that the ``flathub``
remote is registered for the user::
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
Assuming that the ``app.flatpak`` file is in the current directory, install it
for the user by running the ``install`` command::
flatpak --user install app.flatpak
Flatpak will resolve the dependencies of the bundle using the remote we
registered and ask you if you want to install them if they are not already
present. It will then install the application itself.
You can run the application using ``flatpak`` in the usual way::
flatpak run com.example.app_resources
When you are finished with the application and want to uninstall it, use the
following command::
flatpak uninstall com.example.app_resources
You can also uninstall the runtime needed by the application, but it may be
useful to keep it installed for future use.
.. _App_Resources_tutorial_resource:
Resource File
=============
.. contents::
:local:
Inside the ``src`` directory are a collection of files and an ``images``
directory. One of the files is an XML file called ``app_resources.gresource.xml``
that describes the resources to be included with the application.
The Resource Description
------------------------
The ``app_resources.gresource.xml`` file is an XML resource description file
that will be compiled by the build system to produce the
``app_resources.gresource`` resource bundle file. This file is then packaged
with the application and can be loaded at run-time.
The XML file itself provides a manifest of the data files to be included in the
resource bundle.
.. literalinclude:: app/src/app_resources.gresource.xml
:language: xml
Two details are important in this description:
* The ``gresource`` element's ``prefix`` attribute defines the path to the
resources for this application. Everything provided by the resource bundle
will be accessible inside the virtual ``/com/example/app_resources``
directory.
* The image file we want to include in the resource bundle is specified as a
file path inside the ``file`` element. The path is relative to the location
of the resource description file on the workstation's file system.
When the ``app_resources.gresource.xml`` file is processed by the build system,
the tool used to compile it to a bundle locates the image in the directory
structure:
* **app_resources.gresource.xml**
* **images/**
* **picture.svg**
Because the file is specified as ``images/picture.svg`` in the description, an
``images`` directory will be included in the virtual directory structure
contained in the ``app_resources.gresource`` resource bundle file.
Accessing Resources
-------------------
Since the resource description included a prefix and the ``picture.svg`` image
is stored in the ``images`` directory, the application needs to combine the
prefix and the path in order to access its data::
/com/example/app_resources/images/picture.svg
The use of a prefix means that an application can load multiple resources file
from different producers and, in theory, the paths used to access resources
should not overlap. This provides a unified way to access resources via a
single mechanism.
Summary
-------
In this part of the tutorial we saw that resource description files provide
information about the files to be included in a resource bundle, and we looked
at how they specify a directory structure for them. More details about this
type of file can be found in :ref:`gnome_application_resources`.
Next, we will look at the application's source code and see how resources are
accessed at run-time.
.. _App_Resources_tutorial_src:
Source Files
============
.. contents::
:local:
Three of the files in the ``src`` directory contain Python source code:
``app-resources.in``, ``__init__.py`` and ``main.py``.
The first of these is a template that the build system will use to create the
application's ``app-resources`` executable.
The other two files are Python modules that will be copied by the build system
into a Python package directory when the application is installed, resulting in
a directory structure like this:
* **app_resources/**
* **__init__.py**
* **main.py**
The ``main.py`` file contains the Python source code for the application, and
the ``__init__.py`` file is just an empty package file to allow the ``main``
module to be imported from the ``app_resources`` package.
The Program
-----------
Because the application is very simple, we show the whole main program here to
provide an overview before looking at the details:
.. literalinclude:: app/src/main.py
After the opening comments, there are three parts to the program: the module
imports, the application class, and the ``main`` function at the end. We will
examine these parts of this program individually.
Importing Modules
~~~~~~~~~~~~~~~~~
The program begins by importing the modules it needs to create a user
interface. These are the ``sys`` module, which is needed to access the
command line arguments passed to the program when it is run, and the ``gi``
module, which provides a Python interface to the GNOME libraries:
.. literalinclude:: app/src/main.py
:start-at: import
:end-at: repository
When importing the ``Gtk`` module, it is important to specify the version of
the API that will be used.
Application Class
~~~~~~~~~~~~~~~~~
The application is represented by the ``Application`` class which is derived
from the standard ``Gtk.Application`` class. This class provides methods to set
up the application and perform tasks when it is run. It is defined in the
normal way, beginning with the ``__init__`` method:
.. literalinclude:: app/src/main.py
:start-at: class Application
:end-at: GLib.set_application_name
This method performs two tasks that are necessary for the application to run
correctly:
1. It uses the ``super`` built-function to call the ``__init__`` method of the
base class. This associates the application with the application ID given.
This ID must have a certain format which is described in the
`Gio.GApplication documentation`_.
2. It calls the ``GLib.set_application_name`` function to set a user-readable
application name that will be localized if translations are available.
When the application is run, the ``do_active`` method of the ``Application``
class is called. This is something that we need to implement if we want the
application to do something. In this case, we create a window and give it an
icon. Then we add an image to the window and show it:
.. literalinclude:: app/src/main.py
:start-at: do_activate
:end-at: show_all
Going into detail, we create an instance of the ``Gtk.ApplicationWindow``
class, passing the application instance to it so that the application runs
until the window is closed -- see the `application property`_ documentation for
more information.
An image is displayed in the window using an instance of the ``Gtk.Image``
class which we create by calling the static ``new_from_resource`` method to
obtain the image data from the application's resources. The image data is
referred to using a *resource path* which specifies where in the resources the
data is found. This is like referring to a file in a filing system.
We described how this is defined in the :ref:`previous part <App_Resources_tutorial_resource>`
of this tutorial.
Because the window is a container, the label is added to it using the ``add``
method, and the window is shown using the ``show_all`` method so that both the
window and its contents are displayed.
Creating and Running an Application Instance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The last part of the program contains a ``main`` function that is called by
the application's executable script which we describe in the next section:
.. literalinclude:: app/src/main.py
:start-at: def main
Here, we create the ``Application`` instance and call its ``run`` method with
any arguments that were passed to the application from its environment. When it
has finished running, its exit code is returned via the normal ``sys.exit``
call.
.. _App_Resources_tutorial_src_template:
The Template Executable
-----------------------
The ``app-resources.in`` file is a template for the ``app-resources`` file that
is run when the application is launched. We show the file here to provide an
overview, but break it into two parts:
.. literalinclude:: app/src/app-resources.in
:end-at: signal.signal
We create the ``app-resources`` file from this template because the
installation directory and application version are provided by the build
system. These constants are inserted into the template to replace the
``@VERSION@`` and ``@pkgdatadir@`` placeholders. The package directory is
appended to the list of directories that Python uses to find modules.
The second part of the file contains code to load the resources from the
``app_resources.gresource`` resource bundle file, using a function from the
``Gio`` module. It then registers the resources so that they can be used
throughout the application:
.. literalinclude:: app/src/app-resources.in
:start-at: __main__
Finally, the main module for the application is imported and the ``main``
function is run. When this function returns, the application will exit.
Summary
-------
This part of the tutorial showed the simple Python program that forms the core
of the application and the template for the executable script that is
responsible for loading the application's resources and calling the main
program.
However, if we want to install it, we need to build it in a particular way, and
we need to provide files that will allow the user to launch it from a GUI. The
next part of this tutorial describes how we provide the data files to do that.
.. include:: /links.txt
.. _`application property`: https://developer.gnome.org/gtk3/stable/GtkWindow.html#GtkWindow--application
.. _`Application section`: https://python-gtk-3-tutorial.readthedocs.io/en/latest/application.html
#!/usr/bin/env python3
from os import environ, path
from subprocess import call
prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
datadir = path.join(prefix, 'share')
destdir = environ.get('DESTDIR', '')
# Package managers set this so we don't need to run
if not destdir:
print('Updating icon cache...')
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
print('Updating desktop database...')
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
{
"app-id": "com.example.app_resources",
"runtime": "org.gnome.Platform",
"runtime-version": "3.30",
"sdk": "org.gnome.Sdk",
"command": "app-resources",
"finish-args": [
"--socket=wayland"
],
"modules": [
{
"name": "app_resources",
"buildsystem": "meson",
"builddir": true,
"sources": [
{
"type": "dir",
"path": "."
}
]
}
]
}
[Desktop Entry]
Name=App Resources
Icon=com.example.app_resources
Exec=app-resources
Terminal=false
Type=Application
Categories=
StartupNotify=false
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" height="64" version="1.1" width="64"
viewBox="0 0 100 100">
<path d="M 50,5 l 45,25 l -45,25 l -45,-25 z" stroke="black" fill="#c08040" />
<path d="M 95,30 l 0,40 l -45,25 l 0,-40 z" stroke="black" fill="#a06020" />
<path d="M 5,30 l 45,25 l 0,40 l -45,-25 z" stroke="black" fill="#804010" />
</svg>
configure_file(
input: 'com.example.app_resources.desktop',
output: 'com.example.app_resources.desktop',
copy: true,
install: true,
install_dir: join_paths(get_option('datadir'), 'applications')
)
install_data('com.example.app_resources.svg',
install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps'))
project('app_resources',
version: '0.1.0',
meson_version: '>= 0.48.0',
)
subdir('data')
subdir('src')
meson.add_install_script('build-aux/meson/postinstall.py')
#!@PYTHON@
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Purism SPC
# SPDX-License-Identifier: GPL-3.0+
# Author: David Boddie <david.boddie@puri.sm>
import os
import signal
import sys
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@'
sys.path.insert(1, pkgdatadir)
signal.signal(signal.SIGINT, signal.SIG_DFL)
if __name__ == '__main__':
import gi
from gi.repository import Gio
res = Gio.Resource.load(os.path.join(pkgdatadir, 'app_resources.gresource'))
# Register the resource globally within the application.
res._register()
from app_resources import main
sys.exit(main.main(VERSION))
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/example/app_resources">
<file>images/picture.svg</file>
</gresource>
</gresources>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" height="600" version="1.1" width="600"
viewBox="0 0 600 600">
<defs>
<linearGradient id="background" x2="0%" y2="100%" spreadMethod="reflect">
<stop offset="0%" stop-color="#202040" opacity="0" />
<stop offset="100%" stop-color="#202020" />
</linearGradient>
<radialGradient id="sphere" fx="75%" fy="25%">
<stop offset="0%" stop-color="white" />
<stop offset="25%" stop-color="#a0ffa0" />
<stop offset="100%" stop-color="#002000" />
</radialGradient>
</defs>