🧩 .deb and .rpm for Java Guide

A side-by-side manual: build one jar that runs on Java 17 and 21, package it as a .deb and an .rpm, see the runtime installed by apt and dnf on machines with no Java, compare what the tools say, and learn how removal differs.

Video: deb and .rpm for Java Guide (Sysadmin video)
Prefer to watch? This short video runs this page's main commands in a real lab, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.
dpkg-deb --root-owner-group --build cmd-hangman-java_1.0.0_all
rpmbuild -ba ~/rpmbuild/SPECS/cmd-hangman-java.spec
sudo apt install ./cmd-hangman-java_1.0.0_all.deb      |      sudo dnf install ./cmd-hangman-java-1.0.0-1.el9.noarch.rpm

The Python game in the earlier packaging guides needed an interpreter that every machine already has. A Java program needs a runtime that many machines do not have, so packaging it shows the part of packaging that matters most: dependencies. This guide builds the Java hangman jar (from the guide Java Game: Build Linux Command Hangman in Java, Type Along) into a .deb on Ubuntu and an .rpm on Rocky Linux, side by side, and installs both on machines with no Java at all.

You will see the version trap that catches nearly everyone who builds a jar on one machine and runs it on another, how the same dependency is written in each format, how rpmbuild compiles the program while dpkg-deb only packs files, and how apt and dnf differ when you remove the package. It is also the comparison of the two formats: a table of every task in both, at the end.

Every example ran for real on Ubuntu 24.04 (OpenJDK 21, dpkg 1.22.6, lintian 2.117.0) and Rocky Linux 9.8 (OpenJDK 17, rpm 4.16.1.3, rpmlint 1.11), as an ordinary user, with a second clean machine of each for the installs. The word in the game examples is fixed with the HANGSEED variable.

What changes when the program is Java

The Python game in the first guides needed nothing but a text file and an interpreter that every machine has. A Java program changes four things about the package, and this guide goes through each of them for a .deb on Ubuntu and an .rpm on Rocky Linux, side by side:

the runtime       the program needs a Java runtime (JRE) to run. The package must say so, so that apt and dnf install one on a machine that has none. This is the first time the dependency is a big one
the files         the program is one jar file. Linux puts jars in /usr/share/java, and the package adds a one-line launcher script in /usr/bin so that you can start it by name
the build         a .deb is made from files you have already built, but an .rpm is made by running the build, so the spec file calls the compiler. The tools for it are build requirements, not runtime requirements
the version       Java bytecode records the version of the compiler. A jar built with a newer JDK than the runtime that runs it fails. You will see that error, and the one option that prevents it

The program is Hangman.java from the Java guide in the series (Java Game: Build Linux Command Hangman in Java, Type Along). It is architecture-independent bytecode, so both packages are "all" (deb) and "noarch" (rpm).

The labs were an Ubuntu 24.04 machine with OpenJDK 21 and a Rocky Linux 9.8 machine with OpenJDK 17, and a second, clean machine of each without any Java:

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 24.04.5 LTS"
$ java -version 2>&1 | head -1
openjdk version "21.0.12.1" 2026-08-18
$ cat /etc/rocky-release
Rocky Linux release 9.8 (Blue Onyx)
$ java -version 2>&1 | head -1
openjdk version "17.0.21" 2026-10-20 LTS

One jar for both: the version trap

Build the jar on the Ubuntu machine, the way the Java guide did, and see which Java version it was built for. javap is the class-file inspector that comes with the JDK; "major version" 65 is Java 21, 61 is Java 17:

$ ls
Hangman.java
$ javac Hangman.java
$ javap -v Hangman.class | grep "major version"
  major version: 65
$ jar --create --file hangman.jar --main-class Hangman Hangman.class

Take that jar to the Rocky machine, which has Java 17, and run it:

$ java -version 2>&1 | head -1
openjdk version "17.0.21" 2026-10-20 LTS
$ java -jar hangman.jar </dev/null 2>&1 | head -4
Error: LinkageError occurred while loading main class Hangman
	java.lang.UnsupportedClassVersionError: Hangman has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 61.0

The message is long, but the part to read is "class file version 65.0" against "up to 61.0": the jar was built for Java 21 and the runtime knows Java 17. The fix is to tell the compiler which version to produce. --release 17 builds for Java 17 and checks the code against the Java 17 library, so it also runs on 21:

$ javac --release 17 Hangman.java
$ javap -v Hangman.class | grep "major version"
  major version: 61
$ jar --create --file hangman.jar --main-class Hangman Hangman.class
$ sh -c 'printf "c\nh\nm\no\nd\nn\n" | java -jar hangman.jar | tail -3'
You got it! The command was: chmod

Play again? (y/n) Happy hacking!
$ sh -c 'printf "c\nh\nm\no\nd\nn\n" | java -jar hangman.jar | tail -3'
You got it! The command was: chmod

Play again? (y/n) Happy hacking!

The same jar now runs on both. The packages below say "Java 17 or newer" because that is what the jar needs. (The word is fixed with HANGSEED in these examples.)

The launcher script

A jar is started with java -jar PATH. Nobody wants to type that, and the path may change, so the package ships a small script, in /usr/bin, that does it. exec replaces the shell with Java, so the script leaves nothing behind, and "$@" passes any arguments on:

$ cat cmd-hangman-java
#!/bin/sh
exec java -jar /usr/share/java/cmd-hangman-java.jar "$@"
$ chmod 755 cmd-hangman-java
$ ls -l cmd-hangman-java
-rwxr-xr-x 1 tc tc 67 Oct 31 03:48 cmd-hangman-java

The jar will be installed as /usr/share/java/cmd-hangman-java.jar in both packages. The package name is cmd-hangman-java, so it can be installed next to the Python version (cmd-hangman) without any file in common.

The .deb: folder, control file, documents

A .deb is built from a folder laid out like the target system, the same as in the Python guide. Put the jar and the launcher in place, and write the control file. Debian's rule for jars is a versioned file name (cmd-hangman-java-1.0.0.jar) plus an unversioned symbolic link (cmd-hangman-java.jar) that programs use; lintian warns with bad-jar-name about a jar without a version in its name. The launcher uses the link, so it does not change when the version does. The difference from the Python control file is one line, Depends. default-jre-headless is the Ubuntu name for "the standard Java runtime without graphics"; the alternative after the bar, java17-runtime-headless, is what any package providing Java 17 offers, so a machine that already has a suitable Java is not forced to install another:

$ install -D -m755 cmd-hangman-java cmd-hangman-java_1.0.0_all/usr/bin/cmd-hangman-java
$ install -D -m644 hangman.jar cmd-hangman-java_1.0.0_all/usr/share/java/cmd-hangman-java-1.0.0.jar
$ ln -s cmd-hangman-java-1.0.0.jar cmd-hangman-java_1.0.0_all/usr/share/java/cmd-hangman-java.jar
$ mkdir -p cmd-hangman-java_1.0.0_all/DEBIAN cmd-hangman-java_1.0.0_all/usr/share/doc/cmd-hangman-java cmd-hangman-java_1.0.0_all/usr/share/man/man1
$ cat cmd-hangman-java_1.0.0_all/DEBIAN/control
Package: cmd-hangman-java
Version: 1.0.0
Section: utils
Priority: optional
Architecture: all
Depends: default-jre-headless | java17-runtime-headless
Maintainer: TechCommune <admin@techcommune.org>
Description: Linux command hangman for the terminal, in Java
 Guess the Linux command one letter at a time before the
 stick figure is finished. The Java version of the game from
 the packaging series on techcommune.org.

The copyright file, the changelog and the manual page are the same three documents as in the Python .deb guide, with the new name. The changelog and the man page are compressed:

$ cat cmd-hangman-java_1.0.0_all/usr/share/doc/cmd-hangman-java/copyright
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: cmd-hangman-java
Source: https://techcommune.org

Files: *
Copyright: 2026 TechCommune
License: CC0-1.0
 On Debian systems, the complete text of the CC0 1.0 Universal
 license can be found in /usr/share/common-licenses/CC0-1.0.
$ cat changelog-tmp
cmd-hangman-java (1.0.0) unstable; urgency=low

  * First release.

 -- TechCommune <admin@techcommune.org>  Fri, 30 Oct 2026 12:00:00 +0000
$ cat cmd-hangman-java.1
.TH CMD-HANGMAN-JAVA 1 "October 2026" "cmd-hangman-java 1.0.0" "TechCommune"
.SH NAME
cmd-hangman-java \- guess the Linux command before the stick figure is finished
.SH SYNOPSIS
.B cmd-hangman-java
.SH DESCRIPTION
.B cmd-hangman-java
picks a Linux command at random, shows a hint and one underscore per letter.
Type one letter at a time. Six wrong letters finish the stick figure and end
the round. The program needs a Java runtime, version 17 or newer.
.SH AUTHOR
TechCommune, https://techcommune.org
$ gzip -9n -c changelog-tmp > cmd-hangman-java_1.0.0_all/usr/share/doc/cmd-hangman-java/changelog.gz
$ gzip -9n -c cmd-hangman-java.1 > cmd-hangman-java_1.0.0_all/usr/share/man/man1/cmd-hangman-java.1.gz
$ rm changelog-tmp
$ tree cmd-hangman-java_1.0.0_all
cmd-hangman-java_1.0.0_all
|-- DEBIAN
|   `-- control
`-- usr
    |-- bin
    |   `-- cmd-hangman-java
    `-- share
        |-- doc
        |   `-- cmd-hangman-java
        |       |-- changelog.gz
        |       `-- copyright
        |-- java
        |   |-- cmd-hangman-java-1.0.0.jar
        |   `-- cmd-hangman-java.jar -> cmd-hangman-java-1.0.0.jar
        `-- man
            `-- man1
                `-- cmd-hangman-java.1.gz

10 directories, 7 files

The .deb: build and check

Set the permissions, build, and check with lintian, as before:

$ find cmd-hangman-java_1.0.0_all -type d -exec chmod 755 {} +
$ find cmd-hangman-java_1.0.0_all -type f -exec chmod 644 {} +
$ chmod 755 cmd-hangman-java_1.0.0_all/usr/bin/cmd-hangman-java
$ dpkg-deb --root-owner-group --build cmd-hangman-java_1.0.0_all
dpkg-deb: building package 'cmd-hangman-java' in 'cmd-hangman-java_1.0.0_all.deb'.
$ lintian cmd-hangman-java_1.0.0_all.deb && echo "lintian: no complaints"
lintian: no complaints
$ dpkg-deb -I cmd-hangman-java_1.0.0_all.deb | grep -E "Depends|Architecture|Package"
 Package: cmd-hangman-java
 Architecture: all
 Depends: default-jre-headless | java17-runtime-headless
$ dpkg-deb -c cmd-hangman-java_1.0.0_all.deb
drwxr-xr-x root/root         0 2026-10-31 03:48 ./
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/bin/
-rwxr-xr-x root/root        67 2026-10-31 03:48 ./usr/bin/cmd-hangman-java
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/share/
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/share/doc/
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/share/doc/cmd-hangman-java/
-rw-r--r-- root/root       142 2026-10-31 03:48 ./usr/share/doc/cmd-hangman-java/changelog.gz
-rw-r--r-- root/root       318 2026-10-31 03:48 ./usr/share/doc/cmd-hangman-java/copyright
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/share/java/
-rw-r--r-- root/root      3110 2026-10-31 03:48 ./usr/share/java/cmd-hangman-java-1.0.0.jar
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/share/man/
drwxr-xr-x root/root         0 2026-10-31 03:48 ./usr/share/man/man1/
-rw-r--r-- root/root       325 2026-10-31 03:48 ./usr/share/man/man1/cmd-hangman-java.1.gz
lrwxrwxrwx root/root         0 2026-10-31 03:48 ./usr/share/java/cmd-hangman-java.jar -> cmd-hangman-java-1.0.0.jar

The .rpm: sources and spec file

An .rpm is built from a source tarball by a recipe, so the package contains no prebuilt jar: the build compiles it. Make the source folder, in the usual name-version form, with the program, the launcher and the three documents:

$ rpmdev-setuptree
$ mkdir cmd-hangman-java-1.0.0
$ cp Hangman.java cmd-hangman-java-1.0.0/
$ cat cmd-hangman-java-1.0.0/cmd-hangman-java
#!/bin/sh
exec java -jar /usr/share/java/cmd-hangman-java.jar "$@"
$ cat cmd-hangman-java-1.0.0/README.md
# cmd-hangman-java

Guess the Linux command, one letter at a time, before the stick figure is
finished. The Java version of the tutorial game from https://techcommune.org

Run: cmd-hangman-java
$ cat cmd-hangman-java-1.0.0/LICENSE
CC0 1.0 Universal - Public Domain Dedication

The person who associated this work with this deed has dedicated the work
to the public domain by waiving all of his or her rights to the work
worldwide under copyright law, including all related and neighbouring
rights, to the extent allowed by law. You can copy, modify, distribute and
perform the work, even for commercial purposes, all without asking
permission. Full text: https://creativecommons.org/publicdomain/zero/1.0/
$ cat cmd-hangman-java-1.0.0/cmd-hangman-java.1
.TH CMD-HANGMAN-JAVA 1 "October 2026" "cmd-hangman-java 1.0.0" "TechCommune"
.SH NAME
cmd-hangman-java \- guess the Linux command before the stick figure is finished
.SH SYNOPSIS
.B cmd-hangman-java
.SH DESCRIPTION
.B cmd-hangman-java
picks a Linux command at random, shows a hint and one underscore per letter.
Type one letter at a time. Six wrong letters finish the stick figure and end
the round. The program needs a Java runtime, version 17 or newer.
.SH AUTHOR
TechCommune, https://techcommune.org
$ tar -czf ~/rpmbuild/SOURCES/cmd-hangman-java-1.0.0.tar.gz cmd-hangman-java-1.0.0
$ tar -tzf ~/rpmbuild/SOURCES/cmd-hangman-java-1.0.0.tar.gz
cmd-hangman-java-1.0.0/
cmd-hangman-java-1.0.0/cmd-hangman-java.1
cmd-hangman-java-1.0.0/LICENSE
cmd-hangman-java-1.0.0/README.md
cmd-hangman-java-1.0.0/Hangman.java
cmd-hangman-java-1.0.0/cmd-hangman-java

Now the spec file. Compared with the Python spec there are four differences. BuildRequires names what the build needs (the JDK; rpmbuild refuses to start without it). Requires: java-headless is the runtime dependency: every Java runtime package provides that name. %build now does real work: it compiles for Java 17 and makes the jar, so the package always contains a jar built for the right version. And %install puts the jar in %{_javadir}, which is /usr/share/java:

$ cat ~/rpmbuild/SPECS/cmd-hangman-java.spec
Name:       cmd-hangman-java
Version:    1.0.0
Release:    1%{?dist}
Summary:    Linux command hangman for the terminal, in Java
License:    CC0
URL:        https://techcommune.org
Source0:    %{name}-%{version}.tar.gz
BuildArch:  noarch
BuildRequires: java-17-openjdk-devel
Requires:   java-headless

%description
Guess the Linux command one letter at a time before the
stick figure is finished. The Java version of the game from
the packaging series. Needs a Java runtime, version 17 or newer.

%prep
%setup -q

%build
javac --release 17 Hangman.java
jar --create --file hangman.jar --main-class Hangman Hangman.class

%install
install -D -m 0644 hangman.jar %{buildroot}%{_javadir}/%{name}.jar
install -D -m 0755 cmd-hangman-java %{buildroot}%{_bindir}/cmd-hangman-java
install -D -m 0644 cmd-hangman-java.1 %{buildroot}%{_mandir}/man1/cmd-hangman-java.1

%files
%license LICENSE
%doc README.md
%{_javadir}/%{name}.jar
%{_bindir}/cmd-hangman-java
%{_mandir}/man1/cmd-hangman-java.1*

%changelog
* Fri Oct 30 2026 TechCommune <admin@techcommune.org> - 1.0.0-1
- First package of the Java version
$ rpm --eval '%{_javadir}'
/usr/share/java
$ rpmspec -q --qf '%{name}-%{version}-%{release}.%{arch}\n' ~/rpmbuild/SPECS/cmd-hangman-java.spec
cmd-hangman-java-1.0.0-1.el9.noarch

The .rpm: build and check

Build as an ordinary user. The lines that matter are the compile, the jar and the files written:

$ rpmbuild -ba ~/rpmbuild/SPECS/cmd-hangman-java.spec 2>&1 | grep -E "^(\+ javac|\+ jar|Wrote|error)"
+ javac --release 17 Hangman.java
+ jar --create --file hangman.jar --main-class Hangman Hangman.class
Wrote: /home/tc/rpmbuild/SRPMS/cmd-hangman-java-1.0.0-1.el9.src.rpm
Wrote: /home/tc/rpmbuild/RPMS/noarch/cmd-hangman-java-1.0.0-1.el9.noarch.rpm
$ rpmlint ~/rpmbuild/SPECS/cmd-hangman-java.spec ~/rpmbuild/RPMS/noarch/cmd-hangman-java-1.0.0-1.el9.noarch.rpm
/home/tc/rpmbuild/SPECS/cmd-hangman-java.spec: W: invalid-url Source0: cmd-hangman-java-1.0.0.tar.gz
1 packages and 1 specfiles checked; 0 errors, 1 warnings.
$ rpm -qplv ~/rpmbuild/RPMS/noarch/cmd-hangman-java-1.0.0-1.el9.noarch.rpm
-rwxr-xr-x    1 root     root                       71 Oct 31 03:49 /usr/bin/cmd-hangman-java
drwxr-xr-x    2 root     root                        0 Oct 31 03:49 /usr/share/doc/cmd-hangman-java
-rw-r--r--    1 root     root                      194 Oct 31 03:49 /usr/share/doc/cmd-hangman-java/README.md
-rw-r--r--    1 root     root                     3131 Oct 31 03:49 /usr/share/java/cmd-hangman-java.jar
drwxr-xr-x    2 root     root                        0 Oct 31 03:49 /usr/share/licenses/cmd-hangman-java
-rw-r--r--    1 root     root                      475 Oct 31 03:49 /usr/share/licenses/cmd-hangman-java/LICENSE
-rw-r--r--    1 root     root                      325 Oct 31 03:49 /usr/share/man/man1/cmd-hangman-java.1.gz
$ rpm -qpR ~/rpmbuild/RPMS/noarch/cmd-hangman-java-1.0.0-1.el9.noarch.rpm
/usr/bin/sh
java-headless
rpmlib(CompressedFileNames) <= 3.0.4-1
rpmlib(FileDigests) <= 4.6.0-1
rpmlib(PayloadFilesHavePrefix) <= 4.0-1
rpmlib(PayloadIsZstd) <= 5.4.18-1

If the JDK were missing, the build would stop at once, which is what BuildRequires is for:

$ sudo dnf -y remove java-17-openjdk-devel 2>&1 | tail -1
Complete!
$ rpmbuild -ba ~/rpmbuild/SPECS/cmd-hangman-java.spec 2>&1 | tail -3
setting SOURCE_DATE_EPOCH=1793318400
error: Failed build dependencies:
	java-17-openjdk-devel is needed by cmd-hangman-java-1.0.0-1.el9.noarch
$ sudo dnf -y install java-17-openjdk-devel 2>&1 | tail -1
Complete!

Install on a machine with no Java

The real test is a clean machine. Both of these have no Java at all. Copy the packages over (scp in real life) and install them. The point of the Depends and Requires lines is that a runtime appears by itself:

$ command -v java cmd-hangman-java || echo "no java, no game"
no java, no game
$ sudo apt-get update -qq
$ sudo apt-get install -y ./cmd-hangman-java_1.0.0_all.deb 2>&1 | sed -n '/NEW packages/,/^[0-9]* upgraded/p'
The following NEW packages will be installed:
  adduser alsa-topology-conf alsa-ucm-conf ca-certificates
  ca-certificates-java cmd-hangman-java dbus dbus-bin dbus-daemon
  dbus-session-bus-common dbus-system-bus-common default-jre-headless
  fontconfig-config fonts-dejavu-core fonts-dejavu-mono java-common
  krb5-locales libasound2-data libasound2t64 libavahi-client3
  libavahi-common-data libavahi-common3 libbrotli1 libcups2t64 libdbus-1-3
  libexpat1 libfontconfig1 libfreetype6 libglib2.0-0t64 libglib2.0-data
  libgraphite2-3 libgssapi-krb5-2 libharfbuzz0b libicu74 libjpeg-turbo8
  libjpeg8 libk5crypto3 libkeyutils1 libkrb5-3 libkrb5support0 liblcms2-2
  libnspr4 libnss3 libpcsclite1 libpng16-16t64 libsqlite3-0 libxml2
  openjdk-21-jre-headless openssl shared-mime-info xdg-user-dirs
The following packages will be upgraded:
  libssl3t64
1 upgraded, 51 newly installed, 0 to remove and 6 not upgraded.
$ command -v java cmd-hangman-java
/usr/bin/java
/usr/bin/cmd-hangman-java
$ java -version 2>&1 | head -1
openjdk version "21.0.12.1" 2026-08-18
$ command -v java cmd-hangman-java || echo "no java, no game"
no java, no game
$ sudo dnf -y install ./cmd-hangman-java-1.0.0-1.el9.noarch.rpm 2>&1 | sed -n '/Installing dependencies/,/Transaction Summary/p'
Installing dependencies:
 alsa-lib                  x86_64  1.2.15.3-1.el9           appstream     525 k
 avahi-libs                x86_64  0.8-23.el9               baseos         66 k
 copy-jdk-configs          noarch  4.0-3.el9.0.1            appstream      27 k
 cups-libs                 x86_64  1:2.3.3op2-39.el9_8      baseos        260 k
 dbus-libs                 x86_64  1:1.12.20-8.el9          baseos        151 k
 java-17-openjdk-headless  x86_64  1:17.0.21.0.9-2.2.el9_8  appstream      44 M
 javapackages-filesystem   noarch  6.4.0-1.el9              appstream      10 k
 lksctp-tools              x86_64  1.0.19-3.el9_4           baseos         96 k
 lua                       x86_64  5.4.4-4.el9              appstream     187 k
 lua-posix                 x86_64  35.0-8.el9               appstream     131 k
 nspr                      x86_64  4.39.0-5.el9_8           appstream     134 k
 nss                       x86_64  3.124.0-5.el9_8          appstream     726 k
 nss-softokn               x86_64  3.124.0-5.el9_8          appstream     411 k
 nss-softokn-freebl        x86_64  3.124.0-5.el9_8          appstream     381 k
 nss-sysinit               x86_64  3.124.0-5.el9_8          appstream      18 k
 nss-util                  x86_64  3.124.0-5.el9_8          appstream      90 k
 tzdata-java               noarch  2026e-1.el9_8            appstream     145 k

Transaction Summary
$ command -v java cmd-hangman-java
/usr/bin/java
/usr/bin/cmd-hangman-java
$ java -version 2>&1 | head -1
openjdk version "17.0.21" 2026-10-20 LTS

apt and dnf each chose a runtime from what the dependency line allows, installed it, and then installed the game. Both ended up with a working java. Now play (the word is fixed in these examples):

$ cmd-hangman-java
=== cmd-hangman by TechCommune (Java) ===





_______

Command: _ _ _ _ _
Hint:    change file permissions
Guess a letter: c





_______

Command: c _ _ _ _
Hint:    change file permissions
Guess a letter: h





_______

Command: c h _ _ _
Hint:    change file permissions
Guess a letter: m





_______

Command: c h m _ _
Hint:    change file permissions
Guess a letter: o





_______

Command: c h m o _
Hint:    change file permissions
Guess a letter: d





_______

Command: c h m o d
Hint:    change file permissions

You got it! The command was: chmod

Play again? (y/n) n
Happy hacking!
$ cmd-hangman-java
=== cmd-hangman by TechCommune (Java) ===





_______

Command: _ _ _ _ _
Hint:    change file permissions
Guess a letter: c





_______

Command: c _ _ _ _
Hint:    change file permissions
Guess a letter: h





_______

Command: c h _ _ _
Hint:    change file permissions
Guess a letter: m





_______

Command: c h m _ _
Hint:    change file permissions
Guess a letter: o





_______

Command: c h m o _
Hint:    change file permissions
Guess a letter: d





_______

Command: c h m o d
Hint:    change file permissions

You got it! The command was: chmod

Play again? (y/n) n
Happy hacking!

Ask each system what it installed

The same four questions, in the two tools: what does the package need, what files does it own, which package owns this file, and what is installed. The output is for the Ubuntu machine first, then Rocky:

$ dpkg -s cmd-hangman-java | grep -E "^(Version|Depends)"
Version: 1.0.0
Depends: default-jre-headless | java17-runtime-headless
$ dpkg -L cmd-hangman-java | grep -v -E "^/(usr|usr/share|usr/share/doc)?$"
/.
/usr/bin
/usr/bin/cmd-hangman-java
/usr/share/doc/cmd-hangman-java
/usr/share/doc/cmd-hangman-java/changelog.gz
/usr/share/doc/cmd-hangman-java/copyright
/usr/share/java
/usr/share/java/cmd-hangman-java-1.0.0.jar
/usr/share/man
/usr/share/man/man1
/usr/share/man/man1/cmd-hangman-java.1.gz
/usr/share/java/cmd-hangman-java.jar
$ dpkg -S /usr/share/java/cmd-hangman-java.jar
cmd-hangman-java: /usr/share/java/cmd-hangman-java.jar
$ dpkg -l | grep -E "openjdk|cmd-hangman" | cut -c1-80
ii  cmd-hangman-java              1.0.0                             all
ii  openjdk-21-jre-headless:amd64 21.0.12.1+1-1~24.04.4             amd64
$ rpm -q --requires cmd-hangman-java | grep -v rpmlib
/usr/bin/sh
java-headless
$ rpm -ql cmd-hangman-java
/usr/bin/cmd-hangman-java
/usr/share/doc/cmd-hangman-java
/usr/share/doc/cmd-hangman-java/README.md
/usr/share/java/cmd-hangman-java.jar
/usr/share/licenses/cmd-hangman-java
/usr/share/licenses/cmd-hangman-java/LICENSE
/usr/share/man/man1/cmd-hangman-java.1.gz
$ rpm -qf /usr/share/java/cmd-hangman-java.jar
cmd-hangman-java-1.0.0-1.el9.noarch
$ rpm -qa | grep -E "openjdk|cmd-hangman" | sort
cmd-hangman-java-1.0.0-1.el9.noarch
java-17-openjdk-headless-17.0.21.0.9-2.2.el9_8.x86_64

Notice what the Rocky runtime really is: dnf picked a java-headless provider, and the package list shows which one. If you need exactly one Java version, say so in the Requires line (java-17-openjdk-headless) instead of the generic name.

Remove: who cleans up the runtime

Removing the game raises one question: what about the Java runtime that was installed for it? The two tools answer differently. apt leaves it, marked as "installed automatically", and tells you; you remove such leftovers with autoremove. dnf removes the game and, in the same transaction, the dependencies that nothing else needs:

$ sudo apt-get remove -y cmd-hangman-java 2>&1 | grep -E "^Removing|no longer required"
The following packages were automatically installed and are no longer required:
Removing cmd-hangman-java (1.0.0) ...
$ command -v cmd-hangman-java || echo "game removed"
game removed
$ command -v java
/usr/bin/java
$ sudo apt-get autoremove -y 2>&1 | grep -E "^[0-9]+ upgraded"
0 upgraded, 0 newly installed, 47 to remove and 6 not upgraded.
$ command -v java || echo "java removed"
java removed
$ sudo dnf -y remove cmd-hangman-java 2>&1 | grep -E "^Remove|java-17|cmd-hangman|^Removed" | head -6
 cmd-hangman-java         noarch 1.0.0-1.el9                @@commandline 4.1 k
 java-17-openjdk-headless x86_64 1:17.0.21.0.9-2.2.el9_8    @appstream    186 M
Remove  18 Packages
  Erasing          : cmd-hangman-java-1.0.0-1.el9.noarch                   1/18
  Erasing          : java-17-openjdk-headless-1:17.0.21.0.9-2.2.el9_8.x    2/18
  Running scriptlet: java-17-openjdk-headless-1:17.0.21.0.9-2.2.el9_8.x    2/18
$ command -v cmd-hangman-java || echo "game removed"
game removed
$ command -v java || echo "java removed"
java removed
$ rpm -q java-17-openjdk-headless
package java-17-openjdk-headless is not installed
(exit status 1)

In the Ubuntu run, 47 packages came out with autoremove; on Rocky the runtime went with the game, because dnf's clean_requirements_on_remove setting is on by default. Both are safe because the machine was a test machine. On a machine that matters, read the list before you say yes: another program may use that Java by accident, and a dependency you installed by hand yourself is never removed automatically (apt-mark manual NAME and dnf mark install NAME record that).

Side by side: .deb and .rpm for a Java program

What you did, task by task, in both formats:

task                          .deb (Ubuntu, Debian)                     .rpm (Rocky, RHEL, Fedora)
runtime dependency            Depends: default-jre-headless |           Requires: java-headless
                                java17-runtime-headless
build dependency              none (the jar is built before)            BuildRequires: java-17-openjdk-devel
who compiles                  you, before dpkg-deb                      rpmbuild, in %build
build for the right Java      javac --release 17                        javac --release 17 in %build
where the jar goes            usr/share/java/NAME-1.0.0.jar + link      %{_javadir}/NAME.jar  (/usr/share/java)
launcher script               usr/bin/ in the folder                    install -D ... %{_bindir}/...
architecture                  Architecture: all                         BuildArch: noarch
build command                 dpkg-deb --root-owner-group --build DIR   rpmbuild -ba FILE.spec
checker                       lintian FILE.deb                          rpmlint FILE.rpm
install a local file          apt install ./FILE.deb                    dnf install ./FILE.rpm
what does it need             dpkg -s NAME | grep Depends               rpm -q --requires NAME
files of a package            dpkg -L NAME                              rpm -ql NAME
owner of a file               dpkg -S PATH                              rpm -qf PATH
remove, then the leftovers    apt remove NAME; apt autoremove           dnf remove NAME (takes unneeded deps along)

The one idea behind the whole table: a package is files plus a description of what must be on the machine for them to work. For a script that was an interpreter; for a Java program it is a runtime, and the package manager finds it, installs it, and tracks it so that it can be cleaned up later.

Checklist for packaging a Java program

Before you hand the package to anyone:

1. Build the jar with --release N, where N is the oldest Java you want to support, and say "N or newer" in the dependency.
2. Put the jar in /usr/share/java (.deb: a versioned name and an unversioned link; .rpm: the plain name) and start it from a small launcher in /usr/bin.
3. Depends / Requires names a runtime in its "headless" form unless the program has a window.
4. The jar is architecture-independent: "all" and "noarch", not amd64 or x86_64.
5. The .rpm lists the JDK in BuildRequires; the .deb needs no build dependency because the jar already exists.
6. Run lintian and rpmlint, and install on a machine with no Java to prove that the dependency pulls one in.
7. After testing, remove the package and run autoremove to see that nothing was left behind.

The series: Python Game: Build Linux Command Hangman, Build a .deb Package, Build an .rpm Package, Java Game: Build Linux Command Hangman in Java, and this guide. To go further: a program with dependencies of its own (other jars: use the distributions' own java packages), or a launcher with options such as -Xmx for the memory the program may use.