50. Packaging and Installers¶
A program is finished for its users only when they can install it. Every chapter so far ended with a running program on the developer's machine; this one is about the way from there to a user's desktop. On amadeOS and other Debian systems that is a Debian package: the program, a start script, a desktop entry that puts it into the Front Panel, an icon, and AppStream metadata that Software shows. On Windows it is an MSI installer that brings its own Java runtime.
All amade applications and games are packaged the same way, by a few Gradle tasks in their build.gradle and a packaging/ folder next to the sources. This chapter follows amade-fractals (chapter 30) through every step. Chapter 10 showed the short version for Pad; here is the whole of it.
What you build¶
| Command | Result | For |
|---|---|---|
./gradlew jar |
build/libs/amade-fractals-0.1.0.jar, one jar with all libraries |
every platform with Java 25 |
./gradlew deb |
build/distributions/amade-fractals_0.1.0_all.deb |
amadeOS, Debian, Ubuntu |
./gradlew installToAmadeOs |
the package in ../amadeos/config/packages.chroot/ |
the amadeOS image |
./gradlew windowsInstaller |
build/windows/msi/amade-fractals-0.1.0.msi, with a Java runtime |
Windows |
./gradlew windowsInstaller -PinstallerType=app-image |
build/windows/app-image/amade-fractals/, a folder to unzip anywhere |
Windows, without installing |
And in the repository:
packaging/
amade-fractals.desktop the desktop entry
eu.amadeos.apps.fractals.metainfo.xml the AppStream metadata
windows/amade-fractals.ico the Windows icon, 16 to 256 pixels
src/main/resources/icons/amade-fractals.png the icon, 48 × 48 pixels
docs/screenshots/amade-fractals.png the screenshot Software shows
What you will learn¶
- How to build one jar that runs with
java -jar, and why signed libraries need care. - What a Debian package contains, where each file goes, and what the control file says.
- How a desktop entry's categories decide the Front Panel drawer.
- Which icon sizes a program needs and how to make them.
- How AppStream metadata gives Software a name, a description and a screenshot in five languages.
- How a package gets into the amadeOS image.
- How jpackage and WiX build a Windows installer that needs no Java on the user's machine.
- How CI checks every package, so that a broken one never reaches a user.
One jar with everything¶
Both packages start from the same file: a "fat" jar that holds the program and all its libraries, so that java -jar needs nothing else.
jar {
archiveBaseName = 'amade-fractals'
manifest {
attributes(
'Main-Class': 'eu.amadeos.apps.fractals.bootstrap.AmadeFractalsApplication',
'Implementation-Title': 'amade-fractals',
'Implementation-Version': project.version
)
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
// Signed dependency jars would otherwise invalidate the fat jar.
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA'
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
Main-Classmakes the jar runnable.Implementation-Versionlets the program read its own version (getClass().getPackage().getImplementationVersion()), for an About dialog for example.from { configurations.runtimeClasspath ... }unpacks every library jar into this one. Two libraries can have a file of the same name (licence texts,module-info.class);DuplicatesStrategy.EXCLUDEkeeps the first.- Some libraries are signed: their
META-INFhas a signature (*.SF,*.DSA,*.RSA) over their own classes. Copied into a jar with other classes, the signature no longer fits, and Java refuses to load the jar with aSecurityException. Leave the signatures out.
Tip: try the jar, not the IDE
Before you package, run java -jar build/libs/amade-fractals-0.1.0.jar once. A resource that only the IDE finds (a file under src/ instead of src/main/resources/) or a library that is only compileOnly shows up here, not in the IDE.
The Debian package¶
A Debian package (.deb) is an archive of files in the places where they will be installed, plus a folder DEBIAN/ with a description for the package manager. The deb task builds this tree in build/deb-staging and lets dpkg-deb pack it. No Debian tools beyond dpkg-deb are needed, which every Debian and Ubuntu has.
| Path in the package | What it is |
|---|---|
/opt/amade-fractals/amade-fractals.jar |
the fat jar |
/usr/bin/amade-fractals |
the start script, so the program can be started by name |
/usr/share/applications/amade-fractals.desktop |
the desktop entry: name, icon, categories |
/usr/share/icons/hicolor/48x48/apps/amade-fractals.png |
the icon, in the standard icon theme |
/usr/share/metainfo/eu.amadeos.apps.fractals.metainfo.xml |
the AppStream metadata for Software |
/usr/share/doc/amade-fractals/copyright |
the licence, where Debian expects it |
DEBIAN/control |
name, version, dependencies, description |
/opt/<package> is the place for a program that brings its own files and does not split them over /usr/lib and /usr/share. A jar is such a program.
The files¶
def launcher = new File(binDir, 'amade-fractals')
launcher.text = """#!/bin/sh
exec /usr/bin/java -jar /opt/amade-fractals/amade-fractals.jar "\$@"
"""
launcher.setExecutable(true, false)
copy {
from 'packaging/amade-fractals.desktop'
into appDir
}
// AppStream metadata: name, description and screenshot for amadeOS Software (and a repository's
// appstream-generator).
copy {
from 'packaging/eu.amadeos.apps.fractals.metainfo.xml'
into new File(stagingDir, "usr/share/metainfo")
}
copy {
from 'src/main/resources/icons/amade-fractals.png'
into iconDir
}
copy {
from 'LICENSE'
into docDir
rename { 'copyright' }
}
The start script is two lines. exec replaces the shell with Java, so the process the desktop started is the program itself, and "$@" hands over the arguments unchanged: amade-fractals ~/Pictures/seahorse.fractal opens that file. It is made executable for everyone (setExecutable(true, false)); dpkg-deb keeps the file modes.
The desktop entry and the metainfo are files in packaging/, copied unchanged. Pad generates its desktop entry inside build.gradle (chapter 10), which is fine for a few lines; with names in five languages a file of its own is easier to read and to check.
The control file¶
new File(debianDir, 'control').text = """Package: amade-fractals
Version: ${project.version}
Section: graphics
Priority: optional
Architecture: all
Depends: openjdk-25-jre | java-runtime (>= 25)
Maintainer: amadeOS Project <developers@datazuul.com>
Description: Fractal explorer for amadeOS
Renders the Mandelbrot set, Multibrot, Burning Ship, Tricorn, Celtic,
Buffalo, Phoenix, Magnet and Newton fractals and their Julia sets. Zoom
with a click, a frame or the mouse wheel; deep zooms into the Mandelbrot
set use perturbation with an arbitrary precision reference orbit. Smooth
colouring with palettes and colour cycling, export of large images and
parameter files. Part of the amadeOS desktop.
"""
def debFile = new File(distDir, "amade-fractals_${project.version}_all.deb")
def process = new ProcessBuilder('dpkg-deb', '--root-owner-group', '--build',
stagingDir.absolutePath, debFile.absolutePath)
.redirectErrorStream(true)
.start()
process.inputStream.eachLine { println it }
if (process.waitFor() != 0) {
throw new GradleException('dpkg-deb failed')
}
println "[deb] Created ${debFile.absolutePath}"
- Package is the name for
apt installanddpkg -l. The amade programs use their repository name. - Version comes from
versioninbuild.gradle. apt installs a package over an installed one only when the version is higher, so it must grow with every release.0.1.0-SNAPSHOTsorts after0.1.0for dpkg, not before; for a pre-release write0.1.0~beta1, which sorts before0.1.0. CI now works the version out from the commits and writes it the Debian way (chapter 51). - Section sorts the package in package managers:
graphics,editors,mail,sound,video,web,utils,games. - Architecture: all says that the package runs on every processor. A jar does; a package with a native library would need one package per architecture.
- Depends names what must be installed first.
openjdk-25-jre | java-runtime (>= 25)takes Debian's OpenJDK or any package that provides a Java 25 runtime. Name every other program you start: Imagescan depends onsane-utilsforscanimage, Video onlibmpv2. - Maintainer is the amadeOS project for all amade packages.
- Description has a short first line (shown in lists) and a longer text, every line of it indented by one space.
--root-owner-group makes every file belong to root in the package, whoever built it. Without it the files would belong to the developer's user id on every machine that installs the package.
Try it:
./gradlew deb
dpkg-deb --info build/distributions/amade-fractals_0.1.0_all.deb # the control file
dpkg-deb --contents build/distributions/amade-fractals_0.1.0_all.deb # the files
sudo apt install ./build/distributions/amade-fractals_0.1.0_all.deb # installs it with its dependencies
apt install ./file.deb (with the ./) installs a local file and fetches the dependencies; dpkg -i would install the file alone and complain about missing ones.
The desktop entry¶
The desktop entry is what makes a program appear on the desktop. It follows the freedesktop Desktop Entry Specification, which every Linux desktop reads:
[Desktop Entry]
Type=Application
Version=1.0
Name=amadeOS Fractals
Name[de]=amadeOS Fraktale
Name[fr]=amadeOS Fractales
Name[es]=amadeOS Fractales
Name[it]=amadeOS Frattali
GenericName=Fractal Explorer
GenericName[de]=Fraktal-Explorer
GenericName[fr]=Explorateur de fractales
GenericName[es]=Explorador de fractales
GenericName[it]=Esploratore di frattali
Comment=Explore the Mandelbrot set, Julia sets and other fractals
Comment[de]=Mandelbrot-Menge, Julia-Mengen und andere Fraktale erkunden
Comment[fr]=Explorer l'ensemble de Mandelbrot, les ensembles de Julia et d'autres fractales
Comment[es]=Explorar el conjunto de Mandelbrot, los conjuntos de Julia y otros fractales
Comment[it]=Esplorare l'insieme di Mandelbrot, gli insiemi di Julia e altri frattali
Icon=amade-fractals
Exec=/usr/bin/amade-fractals %F
Terminal=false
Categories=Graphics;Education;Math;
Keywords=fractal;mandelbrot;julia;zoom;burning ship;newton;math;
StartupNotify=true
- Name, GenericName and Comment in the five languages of the program (chapter 10). The desktop picks the one for the user's language.
Commentis the tooltip; keep it one short sentence, the same as the AppStream summary. - Icon is a name, not a file: the desktop looks for
amade-fractals.pngin the icon theme. That is why the package installs the icon underhicolor. - Exec is the command.
%Fstands for the files the user opens with the program (from the file manager's Open With, for example); a program that takes one file at a time writes%f. A program that opens files also lists their types inMimeType=, as Pad does. - Keywords help search: in Software, and in the menus of other desktops.
- StartupNotify=true shows the busy cursor until the first window appears.
Check a desktop entry with desktop-file-validate packaging/amade-fractals.desktop (in the package desktop-file-utils).
Categories and the Front Panel drawers¶
Categories decides where the program appears: in the drawers of the amadeOS Front Panel, in the Application Manager, and in the menus of other desktops. The Front Panel reads the desktop entries of all installed programs and fills its drawers by these categories:
| Categories (any of them) | Front Panel drawer |
|---|---|
FileManager, FileTools |
File Manager |
TextEditor, WordProcessor, then Office, Spreadsheet, Presentation, Calendar |
Text & Office |
Email, ContactManagement, News, then Network, WebBrowser, Chat |
Mail & Internet |
AudioVideo, Audio, Video, Player, Recorder |
Sound & Video |
Graphics, Viewer, RasterGraphics, 2DGraphics, VectorGraphics, Photography, Scanning |
Graphics |
Game and its kinds: ArcadeGame, BoardGame, CardGame, LogicGame, … |
Games |
Printing, HardwarePrinting |
Print Manager |
Settings, DesktopSettings, PackageManager, Monitor, System |
Tools |
Documentation, Help |
Help |
anything else (Utility, Development, …) |
only the Application Manager |
A program appears in every drawer whose category it has, so give it the one main category the specification asks for (Graphics, Office, Game, …) and the more specific ones that fit. Fractals says Graphics;Education;Math;: the Graphics drawer, and the education menus of other desktops. desktop-file-validate gives a hint that Graphics and Education are both main categories and the program may appear twice in a menu; for Fractals that is wanted. The Application Developer's Handbook itself, as a package, says Documentation;Development; and is in the Help drawer.
Icons¶
A program needs its icon in three places, in two formats:
| Where | Format | Size |
|---|---|---|
the window's title bar and the task list (setIconImage) |
PNG in the jar, src/main/resources/icons/<name>.png |
48 × 48 |
| the Front Panel, the Application Manager, Software | the same PNG, installed as /usr/share/icons/hicolor/48x48/apps/<name>.png |
48 × 48 |
the Windows Start menu, desktop shortcut and .exe |
ICO, packaging/windows/<name>.ico |
16, 24, 32, 48, 64, 128, 256 |
The window loads its icon from the jar:
java.net.URL icon = getClass().getResource("/icons/amade-fractals.png");
if (icon != null) setIconImage(new javax.swing.ImageIcon(icon).getImage());
On Linux the icon goes into the hicolor theme, the one every icon theme falls back to. The folder name is the size: an icon in 48x48/apps/ must be 48 × 48 pixels, or desktops scale it wrongly or ignore it. Software looks for the sizes 64, 48, 96, 128, 256 and 32 in this order, then in /usr/share/pixmaps; a program may install several sizes, and the amade programs install 48.
Windows wants one .ico file with several sizes inside, from 16 for lists up to 256 for large icons in Explorer. Draw the icon once, large, and let a tool make the sizes. The games draw their icons in Java2D with a small program in tools/MakeIcon.java, which writes a PNG of any size:
java tools/MakeIcon.java 48 src/main/resources/icons/amade-snake-game.png
java tools/MakeIcon.java 256 build/icon-256.png
python3 -c "from PIL import Image; Image.open('build/icon-256.png').save('packaging/windows/amade-snake-game.ico',
sizes=[(16,16),(24,24),(32,32),(48,48),(64,64),(128,128),(256,256)])"
With ImageMagick instead of Python: convert build/icon-256.png -define icon:auto-resize=256,128,64,48,32,24,16 packaging/windows/<name>.ico. Check the result with identify packaging/windows/<name>.ico, which lists one line per size. The icon files are part of the repository; the tool runs again only when the icon changes.
Tip: small sizes need their own care
An icon scaled down from 256 to 16 pixels often becomes a blur. If the small sizes look wrong, draw them separately (the same picture with thicker lines and fewer details) and put them together into the ICO.
AppStream metadata¶
The desktop entry says how to start a program. AppStream says what it is: a name, a summary, a description, a screenshot, the home page, the licence. Software, the amadeOS program for installing and updating, shows this for every installed amade program, and GNOME Software and KDE Discover read the same files. One file per program, /usr/share/metainfo/<id>.metainfo.xml, as described in the AppStream specification.
The beginning says who the program is:
<component type="desktop-application">
<id>eu.amadeos.apps.fractals</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>EUPL-1.2</project_license>
<name>amadeOS Fractals</name>
<name xml:lang="de">amadeOS Fraktale</name>
<name xml:lang="fr">amadeOS Fractales</name>
<name xml:lang="es">amadeOS Fractales</name>
<name xml:lang="it">amadeOS Frattali</name>
- The id is a reverse domain name that never changes:
eu.amadeos.apps.<name>for the applications,eu.amadeos.games.<name>for the games. The file is named after it. - metadata_license is the licence of this file.
CC0-1.0lets every catalogue copy it; project_license is the program's,EUPL-1.2for all amade programs. - name, and after it summary and description, come once without
xml:lang(English) and once for each language. In the description every paragraph and every list item is repeated with itsxml:lang. Keep the summary short and without a full stop at the end; it is the line under the name in lists.
The rest links the metadata to the package and to the web:
<launchable type="desktop-id">amade-fractals.desktop</launchable>
<icon type="stock">amade-fractals</icon>
<categories>
<category>Graphics</category>
<category>Education</category>
<category>Math</category>
</categories>
<url type="homepage">https://github.com/datazuul/amade-fractals</url>
<url type="bugtracker">https://github.com/datazuul/amade-fractals/issues</url>
<developer id="eu.amadeos">
<name>amadeOS Project</name>
</developer>
<screenshots>
<screenshot type="default">
<caption>Seahorse Valley of the Mandelbrot set</caption>
<caption xml:lang="de">Seepferdchental der Mandelbrot-Menge</caption>
<caption xml:lang="fr">La vallée des hippocampes de l'ensemble de Mandelbrot</caption>
<caption xml:lang="es">El valle de los caballitos de mar del conjunto de Mandelbrot</caption>
<caption xml:lang="it">La valle dei cavallucci marini dell'insieme di Mandelbrot</caption>
<image>https://raw.githubusercontent.com/datazuul/amade-fractals/main/docs/screenshots/amade-fractals.png</image>
</screenshot>
</screenshots>
<provides>
<binary>amade-fractals</binary>
</provides>
<content_rating type="oars-1.1"/>
</component>
- launchable names the desktop entry, so that Software can start the program. icon type="stock" is the icon name from the desktop entry.
- categories repeat those of the desktop entry.
- url and developer point to the repository and its issues, and say that the amadeOS project makes the program.
- screenshots: one picture, with a caption in each language. The image is a web address,
https://raw.githubusercontent.com/datazuul/<repo>/main/docs/screenshots/<repo>.png, so the picture lives in the repository and not in the package. Take it on amadeOS with the program doing what it is for: a document open, a fractal zoomed, a game in play, never a title screen or an empty window. - provides names the command, so that a search for
amade-fractalsfinds the package. - content_rating follows OARS, the Open Age Rating Service. An empty
oars-1.1rating says "nothing to warn about"; a game with fighting says so, for example<content_attribute id="violence-fantasy">mild</content_attribute>.
Check the file with appstreamcli validate --no-net packaging/eu.amadeos.apps.fractals.metainfo.xml (in the package appstream). --no-net skips the check of the web addresses, which the screenshot only passes once it is on main. Warnings about a summary that is too long or ends with a full stop are worth fixing; Software cuts long summaries.
Later: a package repository of our own
Today the amade packages come with the amadeOS image, and Software reads the metadata of installed packages. When amadeOS gets a package repository of its own, its catalogue (DEP-11 files, next to the package lists) is made from these same files by appstream-generator, which also downloads the screenshots and turns the icons into the sizes catalogues need. A repository tool such as aptly publishes the packages, not this catalogue; that is an extra step after publishing. Nothing changes in the programs.
Into the amadeOS image¶
The amadeOS image installs every package in its folder config/packages.chroot/. Every amade program has the same task to put its package there:
tasks.register('installToAmadeOs') {
dependsOn 'deb'
description = 'Builds and drops the .deb package into ../amadeos/config/packages.chroot/'
group = 'distribution'
doLast {
def targetDir = file('../amadeos/config/packages.chroot')
targetDir.mkdirs()
def debFile = new File(layout.buildDirectory.get().asFile, "distributions/amade-fractals_${project.version}_all.deb")
copy {
from debFile
into targetDir
}
println "[installToAmadeOs] Copied ${debFile.name} to ${targetDir.absolutePath}"
}
}
It expects a checkout of the amadeos repository next to the program's (../amadeos). You rarely call it yourself: buildDistribution.sh in the amadeos repository builds the whole distribution:
- It clones every repository of
repositories.listthat is not checked out yet, and pullsmainof all of them. - It builds amade-commons, then every repository whose
build.gradlehas aninstallToAmadeOstask, against that checkout of amade-commons (the composite build of chapter 2), and removes the packages of an earlier build first. - It builds the handbooks' packages.
- It runs
build.sh, which makes the ISO image with live-build.
So a new program needs two things to be in the image: the installToAmadeOs task, and its repository name in amadeos/repositories.list. ./buildDistribution.sh --apps amade-fractals --quick builds only one program and repacks the previous image, which is much faster while you try a package.
The Windows installer¶
The amade programs are Java, so they run on Windows too. A Windows user does not have Java 25, though, and should not need it. jpackage, which comes with the JDK, builds a Windows installer that contains the program and a Java runtime. Installed, it is an ordinary Windows program with a Start menu entry and an .exe; nobody sees Java.
// Windows installer with jpackage: the fat jar plus a Java runtime, so Windows users need no Java.
// jpackage builds Windows packages only on Windows; MSI and EXE also need WiX Toolset 3 on the PATH.
// ./gradlew windowsInstaller (MSI; output in build/windows/<type>/)
// ./gradlew windowsInstaller -PinstallerType=exe (setup .exe)
// ./gradlew windowsInstaller -PinstallerType=app-image (folder with amade-fractals.exe, no WiX needed)
tasks.register('windowsInstaller') {
dependsOn jar
description = 'Builds a Windows installer (MSI, EXE or app image) with jpackage'
group = 'distribution'
def jdk = javaToolchains.launcherFor { languageVersion = java.toolchain.languageVersion }
doLast {
if (!System.getProperty('os.name').startsWith('Windows')) {
throw new GradleException('jpackage builds Windows packages only on Windows')
}
def type = project.findProperty('installerType') ?: 'msi'
def buildDirFile = layout.buildDirectory.get().asFile
// jpackage bundles everything in --input, so give it a folder with just the fat jar.
def inputDir = new File(buildDirFile, 'jpackage-input')
def destDir = new File(buildDirFile, "windows/$type")
delete(inputDir, destDir)
copy {
from jar.archiveFile
into inputDir
}
def jpackage = jdk.get().metadata.installationPath.file('bin/jpackage.exe').asFile
def args = [jpackage.absolutePath,
'--type', type,
'--name', 'amade-fractals',
// MSI versions are numeric only: 0.1.0-SNAPSHOT becomes 0.1.0
'--app-version', project.version.toString().replaceAll(/[^0-9.].*$/, ''),
'--vendor', 'datazuul',
'--description', 'Fractal explorer',
'--copyright', 'datazuul, licensed under the EUPL-1.2',
'--input', inputDir.absolutePath,
'--main-jar', jar.archiveFile.get().asFile.name,
'--main-class', application.mainClass.get(),
'--icon', file('packaging/windows/amade-fractals.ico').absolutePath,
'--dest', destDir.absolutePath]
if (type != 'app-image') {
args += ['--win-menu', '--win-menu-group', 'amadeOS', '--win-shortcut', '--win-dir-chooser',
'--license-file', file('LICENSE').absolutePath,
// Never change: lets a newer installer replace an installed version.
'--win-upgrade-uuid', '50c20ea0-d94b-45a7-858a-6379f62fa802']
}
def process = new ProcessBuilder(args).redirectErrorStream(true).start()
process.inputStream.eachLine { println it }
if (process.waitFor() != 0) {
throw new GradleException('jpackage failed')
}
destDir.eachFile { println "[windowsInstaller] Created ${it.absolutePath}" }
}
}
- jpackage builds packages only for the system it runs on: a Windows installer only on Windows. The task stops with a clear message elsewhere. CI builds the Windows packages on a Windows machine (below).
- It takes the
jpackage.exeof the Java toolchain (Java 25), not of whatever Java runs Gradle. --inputis a folder that jpackage puts into the installer completely, so the task gives it a folder with just the fat jar.- Three types:
msi, the Windows Installer package that administrators can also roll out;exe, a setup program;app-image, a folder withamade-fractals.exe, the jar and the runtime, which runs from anywhere (a USB stick) without installing.msiandexeneed the WiX Toolset on thePATH: version 3, or with JDK 25 also 4 or 5.app-imageneeds nothing. --app-version: Windows installers know only numbers and dots, so0.1.0-SNAPSHOTbecomes0.1.0.--win-menu --win-menu-group amadeOSputs the program into an amadeOS folder of the Start menu,--win-shortcutonto the desktop.--win-dir-chooserlets the user choose the folder,--license-fileshows the licence before installing.--win-upgrade-uuidis the program's identity for Windows. A newer installer with the same UUID and a higher version replaces the installed one instead of installing a second copy. Make a new UUID once for each program (uuidgen) and never change it.
The MSI is more than 30 MB, most of it the Java runtime. jpackage takes the whole JDK runtime; --jlink-options or --runtime-image with a runtime made by jlink from just the modules the program uses (java.desktop, java.prefs, java.net.http, …) makes it smaller, at the price of an error at run time when a module is missing.
To build it yourself on Windows, install a JDK 25 and WiX 3 (from the WiX Toolset web site, or choco install wixtoolset), then:
Tip: test the installed program, not the build
Install the MSI on a Windows that has never seen Java, start the program from the Start menu, open a file, and uninstall it again in Settings → Apps. The installed program runs with a different working directory than Gradle and without your user's Java settings, and that is where paths and resources that worked during development break.
Checking it in CI¶
A package nobody tried is a package that may be broken. Each repository's CI builds the packages on every push and pull request and checks what is in them. On Linux, after the tests:
- name: Check the fat jar and the package
run: |
jar=$(ls build/libs/amade-fractals-*.jar)
unzip -l "$jar" icons/amade-fractals.png
unzip -p "$jar" META-INF/MANIFEST.MF | grep -q 'Main-Class: eu.amadeos.apps.fractals'
deb=$(ls build/distributions/amade-fractals_*_all.deb)
dpkg-deb --info "$deb"
dpkg-deb --contents "$deb" > contents.txt
for f in opt/amade-fractals/amade-fractals.jar usr/bin/amade-fractals \
usr/share/applications/amade-fractals.desktop \
usr/share/icons/hicolor/48x48/apps/amade-fractals.png \
usr/share/metainfo/eu.amadeos.apps.fractals.metainfo.xml usr/share/doc/amade-fractals/copyright; do
grep -q "$f" contents.txt || { echo "missing in the package: $f"; exit 1; }
done
The jar must contain the icon and the right Main-Class, and the package every file of the table above. Then the package is uploaded as an artifact of the run, so it can be downloaded from the pull request and installed.
A second job runs on Windows. It installs WiX when the machine does not have it:
# jpackage needs the WiX Toolset for MSI; CI installs WiX 3 (JDK 25 also takes WiX 4/5).
- name: Install WiX Toolset 3
shell: pwsh
run: |
if (-not (Get-Command candle.exe -ErrorAction SilentlyContinue)) {
$bin = Get-ChildItem 'C:\Program Files (x86)\WiX Toolset v3*\bin' -Directory -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $bin) {
choco install wixtoolset --no-progress -y
$bin = Get-ChildItem 'C:\Program Files (x86)\WiX Toolset v3*\bin' -Directory | Select-Object -First 1
}
$bin.FullName | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
}
and after the tests builds the MSI and the portable folder, checks them and packs the folder into a zip:
- name: Build the MSI
run: ./gradlew windowsInstaller --no-daemon
- name: Build the portable app image
run: ./gradlew windowsInstaller -PinstallerType=app-image --no-daemon
- name: Check and pack the results
shell: pwsh
run: |
$msi = Get-ChildItem build/windows/msi/*.msi
if ($msi.Count -ne 1) { throw "Expected one MSI, found $($msi.Count)" }
if ($msi.Length -lt 30MB) { throw "MSI is too small to contain a Java runtime: $($msi.Length) bytes" }
if (-not (Test-Path build/windows/app-image/amade-fractals/amade-fractals.exe)) { throw 'amade-fractals.exe missing' }
Compress-Archive -Path build/windows/app-image/amade-fractals -DestinationPath build/windows/amade-fractals-portable.zip
Write-Host "MSI: $($msi.Name), $([math]::Round($msi.Length / 1MB)) MB"
An MSI under 30 MB has no Java runtime inside and would not run on a machine without Java; the check catches that. Both files are uploaded too, so every pull request has a Windows installer to try.
Checklist for a new program¶
versionfromappVersionanddebVersion(chapter 51),jarwithMain-Classand the signature excludes, as above.- A 48 × 48 icon in
src/main/resources/icons/<name>.png, set as the window icon, andpackaging/windows/<name>.icowith the sizes 16 to 256. packaging/<name>.desktopwith names in five languages,Icon=<name>,Exec=/usr/bin/<name> %Fand the categories of the drawer it belongs to.desktop-file-validatereports no errors.packaging/eu.amadeos.apps.<name>.metainfo.xml(oreu.amadeos.games.<name>) with names, summaries and descriptions in five languages, the screenshot address and an OARS rating.appstreamcli validate --no-netpasses.docs/screenshots/<name>.png, taken on amadeOS with the program in use.- The tasks
deb(with the rightSectionandDepends),installToAmadeOsandwindowsInstallerwith a new upgrade UUID. - The CI jobs for both packages, with the file list of the package check, and the version, release and commit jobs of chapter 51.
- The repository name in
amadeos/repositories.list. - Install the package on amadeOS: the program is in its drawer with its icon, starts, and Software shows its description and screenshot.
Exercises¶
- More icon sizes. Install the icon in 32, 64 and 128 pixels as well, made by the same tool. What changes in the
debtask, and which size does Software take now? - A smaller installer. Build the Windows installer with a runtime from
jlinkthat has only the modules Fractals needs.jdeps --print-module-depson the fat jar tells you which. How much smaller is the MSI, and what happens when a module is missing? - A man page. Debian programs that can be started from a terminal have a manual page. Write a short one for
amade-fractals(in themanformat or withpandocfrom Markdown), install it as/usr/share/man/man1/amade-fractals.1.gz, and check it withman amade-fractals. - A desktop entry check in CI. Install
desktop-file-utilsin the build job and letdesktop-file-validatecheckpackaging/amade-fractals.desktop. Which mistakes does it catch that the Front Panel would silently ignore? - Package checks with lintian. Run
lintianon the package. Which of its complaints matter for a program that is part of amadeOS, and which are about rules for packages in Debian itself?
Summary¶
- One fat jar holds the program and its libraries; leave the signatures of signed libraries out.
- A Debian package is the installed files plus
DEBIAN/control. The amade programs put the jar into/opt/<name>, a two-line start script into/usr/bin, and the desktop entry, the 48-pixel icon, the AppStream metadata and the licence into/usr/share.dpkg-deb --root-owner-group --buildpacks it. CI sets the version (chapter 51). - The desktop entry's
Categoriesdecide the Front Panel drawer; itsIconis a name looked up in thehicoloricon theme. - AppStream metadata gives Software the name, summary, description and screenshot in five languages. The same files will feed the catalogue of an amadeOS package repository.
installToAmadeOsand a line inrepositories.listput a program into the amadeOS image;buildDistribution.shbuilds them all.- jpackage with WiX builds a Windows MSI with its own Java runtime; the upgrade UUID never changes.
- CI builds both packages on every change, checks their contents, and keeps them for download.