Internet Defense League script

26 February 2013

Package Java Apps for Ubuntu and Debian

Recently, I've dived into the subject of packaging Java programs into .deb installation packages for easy installation on Debian, Ubuntu and other Debian based systems. Often, when you Google the subject, you're met with guides that assume that you have downloaded a source tarball, i.e. you are packaging someone else's program. But what if you want to package your own program that happens to be programmed in Java? Then you probably don't have source tarballs, make scripts and whatnot, but instead a bunch of .class files and ressource files (e.g. images, sound files, etc.) in a directory hierarchy -- or perhaps one or more .jar files.

The solution is not overly complicated, although the .deb file building process does take a few steps before you have a working .deb package. I made the process easy for myself, writing a Bash script that handles it all, so it's a no-brainer to build a .deb at the next release of my program. It could probably just as well have been an Ant script, but as I happen to be less familiar with that, Bash was an obvious choice.

What I did is basically build up the contents of the deb file myself. I tried to have the result resemble a natively built program .deb as closely as possible. That is, there should be no hazzle for the user. So my main goals were:

  • The user should be able to install the .deb file directly with dpkg or the Software Center / other GUI install tools
  • No need to manually install Java, if the user hasn't already, the package system should handle that, of course
  • The program should execute through a simple terminal command (just type programname, no cd'ing to the program's directory, typing java programname, etc.) and/or through an icon that fully integrates into the GUI; be it the good, old categorized Gnome menu, the Ubuntu Dash, Gnome Shell, etc.
  • The package should adhere to common standards and recommendations as much as possible, as to let the user not experience any error and warning messages and/or other odd problems.
In other words: For the user, the program should behave like any other program and integrate with their system, without the need to pay special attention to the fact that the program is programmed in Java. Suggestions for improvements are of course very welcome.


The Bash Script

Usually, for Java programs larger than a few lines, I use the Eclipse IDE. By default, Eclipse keeps source files in a directory called src, and compiled .class files in another directory (with an otherwise similar subdirectory structure) called bin. You can, of course, change this behaviour in Eclipse, but this guide assumes a directory structure like Eclipse's default one. Also, it assumes that you want to install a complete directory hierarchy containing .class files etc. on the user's system, but the process should be quite similar with jar files.

So, first step is to create a Bash script file to handle the packaging process. Basically, what the script does, is copy all files that should end up in the .deb file to a temporary directory, and then run a command that builds the actual .deb file. Create an empty text file called, e.g. packagedeb.sh in the root of your project, just inside the src directory. If you do this from within Eclipse, the script is automatically copied to the bin directory. Add the following lines to the file:

#!/bin/sh

PACKAGE_NAME="programname"
PACKAGE_VERSION="0.3"
SOURCE_DIR=$PWD
TEMP_DIR="/tmp"

The first line is a must for all Bash scripts. It just tells the system the fact that it's a Bash script. The next lines set up a few variables for easy package name and version number handling, and for changing between where the .class files reside (the "current directory", always stored in the $PWD variable) and the temporary directory that we use for building the .deb file. The package name is what the program will be known as in the package handling system, so make sure to pick one that's not likely to have already been used, so your user doesn't end up with two programs of the same package name on his system. Also, it should contain no spaces, and preferably only lower-case ASCII letters.

mkdir -p $TEMP_DIR/debian/DEBIAN
mkdir -p $TEMP_DIR/debian/lib
mkdir -p $TEMP_DIR/debian/bin
mkdir -p $TEMP_DIR/debian/usr/share/applications
mkdir -p $TEMP_DIR/debian/usr/share/doc/$PACKAGE_NAME

This next part of the script builds the directory hierarchy for the .deb file. Basically, we create a directory called debian (lower-case) in the temporary dir, and here we put files and directories that we would like to put on the user's system. So, if we want to install a file in /usr/share/applications, we create /tmp/debian/usr/share/applications and put a copy of the file there. In this part of the script, still only empty directories, though. We also create a directory called DEBIAN (upper-case) for meta data for the .deb package.

echo "Package: $PACKAGE_NAME" > $TEMP_DIR/debian/DEBIAN/control
echo "Version: $PACKAGE_VERSION" >> $TEMP_DIR/debian/DEBIAN/control
cat debian/control >> $TEMP_DIR/debian/DEBIAN/control

Next, we create a file control, containing deb package meta data, in the DEBIAN directory. The first two lines take the package name and version from the variables that we defined above, and the rest of the file is copied from a file in my Eclipse source hierarchy (I created a subdir "debian" in src containing such files). The rest of the control file is described further down this guide.

cp debian/programname.desktop $TEMP_DIR/debian/usr/share/applications/
cp debian/copyright $TEMP_DIR/debian/usr/share/doc/$PACKAGE_NAME/

Here, we copy a .desktop file, which is a GUI meta data file for the graphical environment, containing info on icon file location, etc. This file should reside in /usr/share/applications when the program is installed on a system. See further down for details.

Then, a file with copyright information expected by Debian systems. Since my program is open source, I used a text similar to the "copyright" section of this page. If this fits your program, put something like that in a file and tell the script to copy it like above. For a full explanation of the licence file, please refer to the Debian Policy.

cp -r ../bin/ $TEMP_DIR/debian/lib/$PACKAGE_NAME

This line copies the complete "bin" directory and its contents (.class files, images, etc.) to the temporary directory, and renames it to whatever you set the $PACKAGE_NAME variable to. If you use .jar files, you would typically copy those here, instead.

gzip -9c $TEMP_DIR/debian/lib/$PACKAGE_NAME/NEWS > $TEMP_DIR/debian/usr/share/doc/$PACKAGE_NAME/changelog.gz

For my program, I have a text file called NEWS which is basically a changelog. I don't follow Debian's standard for changelogs, but I don't think that matters too much, as I'm not building a native Debian package. Debian expects the changelog file to be gzipped at max level, though, so we'll do that, and install it in the user's /usr/share/doc/programname directory.

mv $TEMP_DIR/debian/lib/$PACKAGE_NAME/resources/programname.svg $TEMP_DIR/debian/usr/share/doc/$PACKAGE_NAME/
chmod 644 $TEMP_DIR/debian/usr/share/doc/$PACKAGE_NAME/programname.svg

I have an .svg file containing a vector icon which is here put into what becomes /usr/share/doc/programname on the user's system. Note that instead of copying it from the my program's source tree, I move it from the temporary directory to which I copied the whole directory structure earlier, since there's no need to have the icon twice in the .deb file.

After that, I set permissions to 644 for the file, which is what the Debian system expects for icons.

rm -r $TEMP_DIR/debian/lib/$PACKAGE_NAME/debian
rm $TEMP_DIR/debian/lib/$PACKAGE_NAME/COPYING
rm $TEMP_DIR/debian/lib/$PACKAGE_NAME/packagedeb.sh

Now, remove some more unneeded files from the temporary directory: The whole directory containing meta data (we already copied from there what we need), a file containing the open source license (the copyright file covers this issue), and the very Bash script file that we're currently writing.

echo '#!/bin/sh' > $TEMP_DIR/debian/bin/programname
echo "export CLASSPATH=$CLASSPATH:/lib/$PACKAGE_NAME" >> $TEMP_DIR/debian/bin/programname
echo "java main/MyMainClass $1" >> $TEMP_DIR/debian/bin/programname
chmod 755 $TEMP_DIR/debian/bin/programname

What we need now is an executable file in /bin so that the program can be started by just typing its name. We'll write a simple bash script that adds the program's path to the CLASSPATH and then runs the Java virtual machine on the correct .class file. Last, we set executable permission 755 on the bash script file.

PACKAGE_SIZE=`du -bs $TEMP_DIR/debian | cut -f 1`
PACKAGE_SIZE=$((PACKAGE_SIZE/1024))
echo "Installed-Size: $PACKAGE_SIZE" >> $TEMP_DIR/debian/DEBIAN/control

Now that we're done copying files to the temporary directory, we need to add a line to the control file that will, in the end, give the user an idea of how much space the program will take up after installation. The Debian Policy tells us that this must be expressed as "the integer value of the estimated installed size in bytes, divided by 1024 and rounded up". So, we use du to get the total size of everything we put into the debian dir, and then devide by 1024. Admittedly, we don't specifically round up, but hey -- this is only an estimate, anyway.

chown -R root $TEMP_DIR/debian/
chgrp -R root $TEMP_DIR/debian/

The whole thing needs to be owned by root (both user and group wise) in order for it to be reachable by anyone on the user's system. Note that these two commands require the whole script to be run by root, or with the sudo command.

cd $TEMP_DIR/
dpkg --build debian
mv debian.deb $SOURCE_DIR/$PACKAGE_NAME-$PACKAGE_VERSION.deb
rm -r $TEMP_DIR/debian

Now we have a full directory structure full of files showing how and what we'd like to install on the target system. Therefore, we change directory to /tmp where we put it all, and run dpkg --build which will transform it all into a debian.deb file, which we will move back and rename to something more appropriate. Finally, we clean up after ourselves, by removing the entire directory structure that we created in the temporary directory.

Phew, that was the hard part. Now for the files that were mentioned along the way.


control File

The file named control keeps meta data for the packaging system. Package name and version are generated in my Bash script above, but the rest of the file looks like this:

Section: sound
Priority: optional
Architecture: all
Maintainer: Thomas Pryds <my@email.address>
Description: Short description.
 Longer description that may span multiple
 lines. Indent with a space.
 .
 Even multiple paragraphs. Just remember that empty
 lines should contain a period and nothing else
 (except the space).
Depends: openjdk-8-jre | openjdk-7-jre | openjdk-6-jre | oracle-java8-installer | oracle-java7-installer | oracle-java6-installer
Homepage: http://a.web.page/presenting/theprogram/

Section is a categorization of the program. See this full list to find what fits your program best. Priority would be "optional" in most cases -- unless your building a vital system application, in which case you probably won't be following this guide. For Architecture, most Java programs are "all", unless some parts of it are architecture specific (if you don't know, then they're not). Maintainer is you as the .deb packager of the program (not as author).

The Description is actually two descriptions. First a short one-liner on the same line as the "Description:" label. Then on the next line a longer description that may span several lines and paragraphs. All lines in the long description must start with a space, and empty lines must consist of a space and a period.

Depends names the packages that this program needs in order to work. Perhaps your program makes use of other programs and requires them to be installed on the user's system. If so, list them here. The package system on the user's computer will make sure to have installed all packages listed as dependencies before your package in installed. Since this program requires Java in order to run, we list that here. In this case, both the OpenJDK and the official Oracle Java are accepted ("|" means "or").

The last line provides means to guide the user to your program's webpage.


.desktop File

The myprogram.desktop file contains meta data used by the graphical user interface (GUI) of the user's system. You can view this as the data that provides a clickable link to the program, either in the "start" menu, the Ubuntu Unity Dash, the Gnome Shell, etc. So, if your program is a purely text based one, you probably don't need this file.

[Desktop Entry]
Encoding=UTF-8
Name=Program Name
Name[da]=Programnavn
Comment=This is my program
Comment[da]=Dette er mit program
Exec=/bin/programname
Icon=/usr/share/doc/programname/programname.svg
Terminal=false
Type=Application
Categories=GNOME;Application;Audio;AudioVideo
StartupNotify=true

This file should always start with [Desktop Entry]. Text editors on most recent (Linux) systems use UTF-8 encoding as default, but if you're unsure, check with your editor. The Name line provides the name of the program as it should appear in the graphical environment. This would typically be a more human readable version of the package name, and spaces are welcome. If your package name is "myprogram", this line would probably contain something like "My Program".

The Comment should be rather short; it is used as a tooltip when the mouse is hovering over your icon. Note that you can provide both the Name and Comment fields in alternative languages.

Next comes the full path to the small, executable Bash script that we generated from the main script above. And then the full path to the icon file that we also copied above. Icons may be in either the SVG format for vector icons, or PNG for bitmap icons. I prefer vectors because of their infinite scaling ability (you don't have to provide icons in a bunch of different sizes), and you can easily convert them into bitmap if you want to also distribute your program to e.g. Windows.

Terminal specifies whether you want to run your program in a text command terminal. With most graphical programs, you don't. Type will be "Application" for most ordinary programs.

Most graphical environments categorize programs into these categories. You can see a list of them all here. StartupNotifu (startup notification) is when the user's mouse cursor turns into a little clock, hourglass, or whatever, while your program starts up.


Generate and Check Result

If everything went right, you're now ready to run your deb package generation script. Simply, start up a command line terminal, navigate to where your script is, and type sh packagedeb.sh -- you should now have a properly named .deb file in that directory. Try it out, see if it works; hopefully it does. To install it via the terminal, write sudo dpkg -i yourpackagename.deb.

Before you publish your deb file, you ought to check it for mistakes. There are a lot of policies it should adhere to, which are all described in detail here, and a great way to check whether your package does is via the lintian tool. If it's not installed on your system, type sudo apt-get install lintian. To check your package and pipe the result to the GEdit text editor, write: lintian -i packagename.deb | gedit &

There is ONE thing that this guide doesn't satisfy with lintian: Every executable file placed in /bin should have a man page, which I don't describe here how to do. I think it's not crucial for a graphical program, but others may disagree. I think writing a man page might be a fun project for the future, so keep an eye out to see if suddenly I've updated this guide to include man page writing :-)

If you wondered which kind of program on which I originally based this, otherwise anonymised, guide, take a look at SpotMachine; a program to record and play audio messages for an audience, e.g. commercial spots in supermarkets, malls, etc.

2 comments:

  1. Thanks, that was great. I managed to successfully create a correct .deb file which works fine. But it seems I cannot upload to the repository without a .changes file. I'm stumped so far...

    ReplyDelete