📦 fpm: .deb and .rpm Guide

A hands-on manual: one folder, one options file and two short commands make a .deb and an .rpm of the hangman game. Install them on clean Ubuntu and Rocky machines, upgrade and remove them, and learn when fpm is the right tool and when it is not.

Video: fpm (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.
fpm -s dir -t deb -n cmd-hangman -v 1.0.0 -d python3 cmd-hangman=/usr/bin/cmd-hangman
fpm -s dir -t rpm -n cmd-hangman -v 1.0.0 -d python3 cmd-hangman=/usr/bin/cmd-hangman

The earlier guides in this packaging series built the hangman game the way distribution maintainers do: a folder and a control file for dpkg-deb, a spec file and a tarball for rpmbuild. That is the right way for a package that a distribution will carry. For a package that only your own machines (or your team, or your customers) will install, there is a shorter way: fpm, which builds a .deb or an .rpm directly from the files, with a few command-line options in place of the control and spec files.

This guide builds the Python game and the Java game with fpm, both formats from the same folder; looks at what is inside the packages and at what lintian says fpm leaves out; adds install and remove messages with scripts; upgrades a package with --iteration; installs on clean Ubuntu and Rocky Linux machines; shows why the Java runtime needs a different dependency name in each format; tries converting a finished .deb into an .rpm and shows where that goes wrong; and ends with a table of fpm against the native tools.

Every example ran for real: fpm 1.18.0 with Ruby 3.2.3 on Ubuntu 24.04 (dpkg 1.22.6, lintian 2.117.0), the packages installed on a clean Ubuntu 24.04 and a clean Rocky Linux 9.8 machine, as an ordinary user with sudo. The random word in the game examples is fixed with the HANGSEED variable, so that this page is repeatable; on your machine the word is random.

What fpm is, and when to use it

fpm (Effing Package Management) is a command-line tool that builds a package from files you already have. You tell it where the files should go on the target system, what the package is called and what it needs, and it writes a .deb, an .rpm, or a few other formats. There is no control file to write, no spec file, and no folder tree to lay out by hand. The same command line, with one word changed, makes both formats.

The earlier guides in this series built the game by the native route: dpkg-deb with a folder and a control file, and rpmbuild with a spec file and a source tarball. That is the route that distribution maintainers use, and the one a distribution will accept. fpm is the quick route: it is made for packages you build for your own machines, a team, or a customer, where you want a real package that installs, upgrades and removes cleanly, but you do not need to pass a distribution's review.

This guide builds the Python hangman game with fpm into a .deb and an .rpm from one set of options, looks at what fpm leaves out (lintian will tell us), adds install and remove messages, installs both on clean machines, repeats the exercise for the Java version where the two formats need different dependency names, tries converting a finished package from one format to the other, and ends with a comparison of fpm and the native tools. The game itself is the one from Python Game: Build Linux Command Hangman, so the file is the same everywhere.

Install fpm

fpm is written in Ruby and installs as a gem. To write .rpm files it calls rpmbuild, and to compile its own dependencies the gem needs a compiler, so on Ubuntu install four things first: ruby and ruby-dev, build-essential, and rpm (which provides rpmbuild). Then install the gem. --no-document skips the documentation, which makes the install much faster:

$ sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y ruby ruby-dev build-essential rpm 2>&1 | tail -1
0 upgraded, 0 newly installed, 0 to remove and 5 not upgraded.
$ sudo gem install fpm --no-document 2>&1 | tail -2
Successfully installed fpm-1.18.0
1 gem installed
$ fpm --version
1.18.0

On Rocky Linux, RHEL or Fedora the same tool needs the Rocky names of the same four things: ruby, ruby-devel, gcc and make, and rpm-build. Writing a .deb needs nothing extra, because fpm builds that format itself. The rest of this guide uses the Ubuntu machine for building; the finished packages are then installed on clean machines of both kinds.

$ sudo dnf -y install ruby ruby-devel rubygems rpm-build gcc make 2>&1 | tail -1
Complete!
$ sudo gem install fpm --no-document 2>&1 | tail -2
Successfully installed fpm-1.18.0
1 gem installed
$ fpm --version
1.18.0

Both machines now have the same fpm. Everything below runs on the Ubuntu one.

Put the game and its manual page in place

fpm packages files that exist, so start with the two files: the game and its manual page, from the earlier guide. Copy them into a work folder, make the game executable and the manual page readable by everyone (mode 644; lintian complains otherwise) and check that it runs. The first section of the Python guide explains the game; here it is only a file to ship:

$ chmod 755 cmd-hangman
$ chmod 644 cmd-hangman.1
$ ls -l
total 8
-rwxr-xr-x 1 tc tc 2185 Oct 30 21:57 cmd-hangman
-rw-r--r-- 1 tc tc  482 Oct 30 23:52 cmd-hangman.1

Run one round from this folder with ./ in front, so you know the file is good before you package it (the word is fixed here so the page is repeatable; you will get a random one):

$ ./cmd-hangman
=== cmd-hangman by TechCommune ===





_______

Command: _ _ _ _
Hint:    search text for a pattern
Guess a letter: g





_______

Command: g _ _ _
Hint:    search text for a pattern
Guess a letter: r





_______

Command: g r _ _
Hint:    search text for a pattern
Guess a letter: e





_______

Command: g r e _
Hint:    search text for a pattern
Guess a letter: p





_______

Command: g r e p
Hint:    search text for a pattern

You got it! The command was: grep

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

The first package: a .deb

This is the whole job. -s dir says the source is a set of files (fpm can also take a Python package, a Ruby gem, an npm module, or an existing .deb or .rpm). -t deb says the target is a .deb. -n, -v and -a set the package name, the version and the architecture; all means the same files work on every CPU, because the game is a text script. -d python3 is the dependency. The last two words, SOURCE=DESTINATION, are the heart of it: the file in this folder, and the path it will have on the target system:

$ fpm -s dir -t deb -n cmd-hangman -v 1.0.0 -a all --license CC0 --url https://techcommune.org --maintainer "TechCommune <admin@techcommune.org>" --description "Linux command hangman for the terminal" -d python3 cmd-hangman=/usr/bin/cmd-hangman cmd-hangman.1=/usr/share/man/man1/cmd-hangman.1 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman_1.0.0_all.deb"
$ ls -l *.deb
-rw-r--r-- 1 tc tc 2250 Oct 31 16:33 cmd-hangman_1.0.0_all.deb

That is a finished package, named cmd-hangman_1.0.0_all.deb by fpm. Notice that fpm created the missing parent folders (usr, usr/bin, usr/share, usr/share/man/man1) inside the package by itself.

The same command, an .rpm

Change one word. -t rpm instead of -t deb, and -a noarch, which is the RPM word for "all CPUs" (fpm accepts all and translates it, but noarch is what the .rpm will say, so use the real name):

$ fpm -s dir -t rpm -n cmd-hangman -v 1.0.0 -a noarch --license CC0 --url https://techcommune.org --maintainer "TechCommune <admin@techcommune.org>" --description "Linux command hangman for the terminal" -d python3 cmd-hangman=/usr/bin/cmd-hangman cmd-hangman.1=/usr/share/man/man1/cmd-hangman.1 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman-1.0.0-1.noarch.rpm"
$ ls -l *.deb *.rpm
-rw-r--r-- 1 tc tc 7596 Oct 31 16:33 cmd-hangman-1.0.0-1.noarch.rpm
-rw-r--r-- 1 tc tc 2250 Oct 31 16:33 cmd-hangman_1.0.0_all.deb

Two packages from one folder, with no spec file and no control file. The release number is plain 1 (cmd-hangman-1.0.0-1.noarch.rpm), where a package built with rpmbuild on Rocky would say 1.el9: the el9 is a tag that a spec file's %{?dist} adds, and fpm does not add it.

Put the shared options in a .fpm file

Most of that command line is the same for both formats. fpm reads a file named .fpm in the current folder (a dot file, so ls does not show it) and adds each line as if you had typed it. Put the options that do not change in it, one per line. The only thing left on the command line is what differs: the target and the files:

$ cat .fpm
-s dir
-n cmd-hangman
-v 1.0.0
-a all
--license CC0
--url https://techcommune.org
--maintainer "TechCommune <admin@techcommune.org>"
--description "Linux command hangman for the terminal"
-d python3
$ fpm -t deb cmd-hangman=/usr/bin/cmd-hangman cmd-hangman.1=/usr/share/man/man1/cmd-hangman.1 2>&1 | sed 's/.*:message=>//; s/, :level=>:warn}//'
"Loading flags from rc file .fpm"
"Additional options: -s dir -n cmd-hangman -v 1.0.0 -a all --license CC0 --url https://techcommune.org --maintainer TechCommune <admin@techcommune.org> --description Linux command hangman for the terminal -d python3"
"Created package", :path=>"cmd-hangman_1.0.0_all.deb"}
$ fpm -t rpm -a noarch cmd-hangman=/usr/bin/cmd-hangman cmd-hangman.1=/usr/share/man/man1/cmd-hangman.1 2>&1 | sed 's/.*:message=>//; s/, :level=>:warn}//'
"Loading flags from rc file .fpm"
"Additional options: -s dir -n cmd-hangman -v 1.0.0 -a all --license CC0 --url https://techcommune.org --maintainer TechCommune <admin@techcommune.org> --description Linux command hangman for the terminal -d python3"
"Created package", :path=>"cmd-hangman-1.0.0-1.noarch.rpm"}
$ ls -l *.deb *.rpm
-rw-r--r-- 1 tc tc 7596 Oct 31 16:33 cmd-hangman-1.0.0-1.noarch.rpm
-rw-r--r-- 1 tc tc 2250 Oct 31 16:33 cmd-hangman_1.0.0_all.deb

The -a noarch on the second line overrides the -a all from the file. Notice the first line of each run (Loading flags from rc file .fpm), and the second, which lists the options it added: that is how you know the file was used. The .fpm file can be committed next to your code, so a colleague builds the same package with the same two short commands.

Look inside both packages

Never trust a package you have not looked inside. The .deb first, with dpkg-deb (-I shows the control information, -c the file list). Then the .rpm with rpm (-qpi: information, -qpl: file list, -qpR: what it needs; the p means "a package file", not an installed package):

$ dpkg-deb -I cmd-hangman_1.0.0_all.deb
 new Debian package, version 2.0.
 size 2250 bytes: control archive=461 bytes.
     285 bytes,    12 lines      control
     194 bytes,     3 lines      md5sums
 Package: cmd-hangman
 Version: 1.0.0
 License: CC0
 Vendor: none
 Architecture: all
 Maintainer: TechCommune <admin@techcommune.org>
 Installed-Size: 2
 Depends: python3
 Section: default
 Priority: optional
 Homepage: https://techcommune.org
 Description: Linux command hangman for the terminal
$ dpkg-deb -c cmd-hangman_1.0.0_all.deb
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./usr/
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./usr/bin/
-rwxr-xr-x 0/0            2185 2026-10-30 21:57 ./usr/bin/cmd-hangman
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./usr/share/
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./usr/share/doc/
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./usr/share/doc/cmd-hangman/
-rw-r--r-- 0/0             145 2026-10-31 16:33 ./usr/share/doc/cmd-hangman/changelog.gz
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./usr/share/man/
drwxr-xr-x 0/0               0 2026-10-31 16:33 ./usr/share/man/man1/
-rw-r--r-- 0/0             482 2026-10-30 23:52 ./usr/share/man/man1/cmd-hangman.1
$ rpm -qpi cmd-hangman-1.0.0-1.noarch.rpm
Name        : cmd-hangman
Version     : 1.0.0
Release     : 1
Architecture: noarch
Install Date: (not installed)
Group       : default
Size        : 2667
License     : CC0
Signature   : (none)
Source RPM  : cmd-hangman-1.0.0-1.src.rpm
Build Date  : Sat Oct 31 16:33:43 2026
Build Host  : 66d47c3a8cd1
Relocations : /
Packager    : TechCommune <admin@techcommune.org>
Vendor      : none
URL         : https://techcommune.org
Summary     : Linux command hangman for the terminal
Description :
Linux command hangman for the terminal
$ rpm -qpl cmd-hangman-1.0.0-1.noarch.rpm
/usr/bin/cmd-hangman
/usr/share/man/man1/cmd-hangman.1
$ rpm -qpR cmd-hangman-1.0.0-1.noarch.rpm
python3
rpmlib(CompressedFileNames) <= 3.0.4-1
rpmlib(PayloadFilesHavePrefix) <= 4.0-1

The same facts are in both: name, version, maintainer, licence, home page, dependency on python3, and the two files in their places. Compare with the earlier guides: the fields you typed on the command line are the fields that those guides wrote into a control file and a spec file.

What fpm leaves out

A package builder that asks so little must be skipping something. lintian, the Debian package checker from the .deb guide, shows what:

$ lintian cmd-hangman_1.0.0_all.deb
E: cmd-hangman: extended-description-is-empty
E: cmd-hangman: no-copyright-file
E: cmd-hangman: uncompressed-manual-page [usr/share/man/man1/cmd-hangman.1]
W: cmd-hangman: package-contains-timestamped-gzip 2026-10-31T16:33:42 [usr/share/doc/cmd-hangman/changelog.gz]
W: cmd-hangman: syntax-error-in-debian-changelog "not a Debian changelog" [usr/share/doc/cmd-hangman/changelog.gz:1]
W: cmd-hangman: unknown-field License
W: cmd-hangman: unknown-field Vendor
W: cmd-hangman: unknown-section default

Read the list. A copyright file in /usr/share/doc, a real Debian changelog, a compressed manual page and a long description are things the native guides wrote by hand, and fpm does not. The package works, installs and removes cleanly, and for your own machines that is usually enough. For a package going into the Debian or Ubuntu archives it is not: those need the native route. rpmlint, the checker for the .rpm side, is shown in the Build an .rpm Package guide; a package from fpm gets similar complaints there.

Some of it is easy to fix, and the next section does the three that are worth the effort.

Man page, install messages, and a package revision

Three improvements, all new options on the same commands. First, compress the manual page the way Debian wants it (gzip -9n, as in the .deb guide) and ship that file. Second, give fpm two short scripts to run: --after-install runs when the package has been installed, --before-remove runs just before it is removed. Third, --iteration 2 says this is the second build of version 1.0.0: it changes the file name (1.0.0-2) and tells apt and dnf that this one is newer than the first build, which is what makes an upgrade work. --category utils sets the Section field that lintian complained about:

$ cat post.sh
#!/bin/sh
echo "cmd-hangman is installed. Type: cmd-hangman"
$ cat gone.sh
#!/bin/sh
echo "cmd-hangman is removed. Thanks for playing."
$ gzip -9n -c cmd-hangman.1 > cmd-hangman.1.gz
$ fpm -t deb --category utils --iteration 2 --after-install post.sh --before-remove gone.sh cmd-hangman=/usr/bin/cmd-hangman cmd-hangman.1.gz=/usr/share/man/man1/cmd-hangman.1.gz 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman_1.0.0-2_all.deb"
$ fpm -t rpm -a noarch --iteration 2 --after-install post.sh --before-remove gone.sh cmd-hangman=/usr/bin/cmd-hangman cmd-hangman.1.gz=/usr/share/man/man1/cmd-hangman.1.gz 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman-1.0.0-2.noarch.rpm"
$ ls -l *.deb *.rpm
-rw-r--r-- 1 tc tc 7596 Oct 31 16:33 cmd-hangman-1.0.0-1.noarch.rpm
-rw-r--r-- 1 tc tc 8087 Oct 31 16:33 cmd-hangman-1.0.0-2.noarch.rpm
-rw-r--r-- 1 tc tc 2474 Oct 31 16:33 cmd-hangman_1.0.0-2_all.deb
-rw-r--r-- 1 tc tc 2250 Oct 31 16:33 cmd-hangman_1.0.0_all.deb
$ lintian cmd-hangman_1.0.0-2_all.deb
E: cmd-hangman: debian-changelog-file-missing-or-wrong-name
E: cmd-hangman: extended-description-is-empty
E: cmd-hangman: no-copyright-file
W: cmd-hangman: maintainer-script-empty [postinst]
W: cmd-hangman: maintainer-script-empty [prerm]
W: cmd-hangman: maintainer-script-ignores-errors [postinst]
W: cmd-hangman: maintainer-script-ignores-errors [prerm]
W: cmd-hangman: package-contains-timestamped-gzip 2026-10-31T16:33:49 [usr/share/doc/cmd-hangman/changelog.gz]
W: cmd-hangman: syntax-error-in-debian-changelog "not a Debian changelog" [usr/share/doc/cmd-hangman/changelog.gz:1]
W: cmd-hangman: unknown-field License
W: cmd-hangman: unknown-field Vendor

The uncompressed-manual-page and unknown-section lines are gone. What remains is the part fpm does not do: copyright, changelog, a longer description. The two script warnings are new, because the scripts are one line each; they are fine for a message. To see the scripts that went into the .rpm, ask for them:

$ rpm -qp --scripts cmd-hangman-1.0.0-2.noarch.rpm
postinstall scriptlet (using /bin/sh):
#!/bin/sh
echo "cmd-hangman is installed. Type: cmd-hangman"
preuninstall scriptlet (using /bin/sh):
#!/bin/sh
echo "cmd-hangman is removed. Thanks for playing."

Install on clean machines, upgrade, remove

Copy the first builds (revision 1, without scripts) and the second builds (revision 2, with scripts) to a fresh Ubuntu machine and a fresh Rocky machine, with scp in real life. First the .deb on Ubuntu. Install revision 1, then install revision 2 over it, which is an upgrade, and see the after-install message appear. On this machine python3 was missing, so the first install brought it in with the game: that is the -d python3 at work.

$ sudo apt-get update -qq
$ sudo apt-get install -y ./cmd-hangman_1.0.0_all.deb 2>&1 | grep -E "^(Setting up|Unpacking) (cmd-hangman|python3 )"
Unpacking python3 (3.12.3-0ubuntu2.1) ...
Unpacking cmd-hangman (1.0.0) ...
Setting up python3 (3.12.3-0ubuntu2.1) ...
Setting up cmd-hangman (1.0.0) ...
$ sudo apt-get install -y ./cmd-hangman_1.0.0-2_all.deb 2>&1 | grep -E "^(Setting up|Unpacking|cmd-hangman is)"
Unpacking cmd-hangman (1.0.0-2) over (1.0.0) ...
Setting up cmd-hangman (1.0.0-2) ...
cmd-hangman is installed. Type: cmd-hangman
$ dpkg -s cmd-hangman | grep -E "^(Version|Depends|Section)"
Section: utils
Version: 1.0.0-2
Depends: python3
$ dpkg -L cmd-hangman | grep -E "bin|man1"
/usr/bin
/usr/bin/cmd-hangman
/usr/share/man/man1
/usr/share/man/man1/cmd-hangman.1.gz

Now the .rpm on Rocky: the same two steps. dnf calls the second one an upgrade and prints the message from the script, as apt did:

$ sudo dnf -y install ./cmd-hangman-1.0.0-1.noarch.rpm 2>&1 | grep -E "^ +(Installing|Upgrading) +:"
  Installing       : cmd-hangman-1.0.0-1.noarch                             1/1
$ sudo dnf -y upgrade ./cmd-hangman-1.0.0-2.noarch.rpm 2>&1 | grep -E "^ +(Installing|Upgrading|Cleanup|Running scriptlet).*cmd-hangman|cmd-hangman is"
  Upgrading        : cmd-hangman-1.0.0-2.noarch                             1/2
  Running scriptlet: cmd-hangman-1.0.0-2.noarch                             1/2
cmd-hangman is installed. Type: cmd-hangman
  Cleanup          : cmd-hangman-1.0.0-1.noarch                             2/2
  Running scriptlet: cmd-hangman-1.0.0-1.noarch                             2/2
$ rpm -q cmd-hangman
cmd-hangman-1.0.0-2.noarch
$ rpm -qi cmd-hangman | grep -E "^(Version|Release)"
Version     : 1.0.0
Release     : 2

Play one round on each machine (the word is fixed for the page):

$ cmd-hangman
=== cmd-hangman by TechCommune ===





_______

Command: _ _ _ _
Hint:    search text for a pattern
Guess a letter: g





_______

Command: g _ _ _
Hint:    search text for a pattern
Guess a letter: r





_______

Command: g r _ _
Hint:    search text for a pattern
Guess a letter: e





_______

Command: g r e _
Hint:    search text for a pattern
Guess a letter: p





_______

Command: g r e p
Hint:    search text for a pattern

You got it! The command was: grep

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





_______

Command: _ _ _ _
Hint:    search text for a pattern
Guess a letter: g





_______

Command: g _ _ _
Hint:    search text for a pattern
Guess a letter: r





_______

Command: g r _ _
Hint:    search text for a pattern
Guess a letter: e





_______

Command: g r e _
Hint:    search text for a pattern
Guess a letter: p





_______

Command: g r e p
Hint:    search text for a pattern

You got it! The command was: grep

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

Removing runs the before-remove script on both:

$ sudo apt-get remove -y cmd-hangman 2>&1 | grep -E "^Removing|removed"
Removing cmd-hangman (1.0.0-2) ...
cmd-hangman is removed. Thanks for playing.
$ sudo dnf -y remove cmd-hangman 2>&1 | grep -E "Erasing|Removed|removed"
cmd-hangman is removed. Thanks for playing.
  Erasing          : cmd-hangman-1.0.0-2.noarch                             1/1
Removed:
$ command -v cmd-hangman || echo "game removed"
game removed
$ command -v cmd-hangman || echo "game removed"
game removed

Dependencies that have different names: the Java version

The Python game could use one -d python3 for both, because the package is called python3 on both systems. That is not true for most things. The Java runtime is default-jre-headless on Ubuntu and java-headless on Rocky, so the two builds need a different -d each. This is where fpm's habit of one command per format pays off: you simply write the dependency into each command. First build the jar, the same way as in the Java guides (--release 17, so that the runtime on both systems can run it), and the launcher script:

$ mkdir -p classes
$ javac --release 17 -d classes Hangman.java
$ jar --create --file hangman.jar --main-class Hangman -C classes .
$ cat cmd-hangman-java
#!/bin/sh
exec java -jar /usr/share/java/cmd-hangman-java.jar "$@"
$ chmod 755 cmd-hangman-java

Now the two builds. The .fpm file in this folder belongs to the Python package (it names cmd-hangman and says -d python3), and a -d on the command line is added to it rather than replacing it, so move the file aside for the Java build and give the options on the command line. The .deb offers two runtimes with a bar; the .rpm asks for the generic name that every Java runtime package provides:

$ mv .fpm .fpm.python
$ fpm -s dir -t deb -n cmd-hangman-java -v 1.0.0 -a all --category utils --license CC0 --url https://techcommune.org --maintainer "TechCommune <admin@techcommune.org>" --description "Linux command hangman for the terminal, in Java" -d "default-jre-headless | java17-runtime-headless" cmd-hangman-java=/usr/bin/cmd-hangman-java hangman.jar=/usr/share/java/cmd-hangman-java.jar 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman-java_1.0.0_all.deb"
$ fpm -s dir -t rpm -n cmd-hangman-java -v 1.0.0 -a noarch --license CC0 --url https://techcommune.org --maintainer "TechCommune <admin@techcommune.org>" --description "Linux command hangman for the terminal, in Java" -d java-headless cmd-hangman-java=/usr/bin/cmd-hangman-java hangman.jar=/usr/share/java/cmd-hangman-java.jar 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman-java-1.0.0-1.noarch.rpm"
$ dpkg-deb -I cmd-hangman-java_1.0.0_all.deb | grep Depends
 Depends: default-jre-headless | java17-runtime-headless
$ rpm -qpR cmd-hangman-java-1.0.0-1.noarch.rpm | grep -v rpmlib
java-headless

Now each package asks only for a Java runtime. Install both on the clean machines, which have no Java at all, and watch the runtime arrive with them:

$ command -v java || echo "no java here"
no java here
$ sudo apt-get install -y ./cmd-hangman-java_1.0.0_all.deb 2>&1 | grep -E "^Setting up (default-jre-headless|openjdk-21-jre-headless|cmd-hangman-java)|newly installed"
0 upgraded, 47 newly installed, 0 to remove and 6 not upgraded.
Setting up openjdk-21-jre-headless:amd64 (21.0.12.1+1-1~24.04.4) ...
Setting up default-jre-headless (2:1.21-75+exp1) ...
Setting up cmd-hangman-java (1.0.0) ...
$ java -version 2>&1 | head -1
openjdk version "21.0.12.1" 2026-08-18
$ command -v java || echo "no java here"
no java here
$ sudo dnf -y install ./cmd-hangman-java-1.0.0-1.noarch.rpm 2>&1 | grep -E "^ (cmd-hangman-java|java-17-openjdk-headless) |^Install +[0-9]+ Package"
 cmd-hangman-java          noarch  1.0.0-1                  @commandline  9.3 k
 java-17-openjdk-headless  x86_64  1:17.0.21.0.9-2.2.el9_8  appstream      44 M
Install  18 Packages
$ java -version 2>&1 | head -1
openjdk version "17.0.21" 2026-10-20 LTS
$ 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!

Convert a finished package: -s deb -t rpm

fpm can also take an existing package as its source and write the other format. That sounds like a shortcut for avoiding the second build, and it works for simple packages: the Python .deb becomes an .rpm in one command, with the name, version and files carried over:

$ cd conv && cp ../python-pkgs/cmd-hangman_1.0.0-2_all.deb . && fpm -s deb -t rpm cmd-hangman_1.0.0-2_all.deb 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman-1.0.0-2.noarch.rpm"
$ cd conv && rpm -qpi cmd-hangman-1.0.0-2.noarch.rpm | grep -E "^(Name|Version|Release|License)"
Name        : cmd-hangman
Version     : 1.0.0
Release     : 2
License     : CC0
$ cd conv && rpm -qpR cmd-hangman-1.0.0-2.noarch.rpm | grep -v rpmlib
/bin/sh
/bin/sh
python3

The same trick has two traps, and the Java package finds both. The first: the .deb says "default-jre-headless | java17-runtime-headless", and the .rpm format has no bar. fpm copies the line as it is and rpmbuild refuses it (the real message needs fpm's --debug option to show):

$ cd conv && cp ../cmd-hangman-java_1.0.0_all.deb . && fpm --debug -s deb -t rpm cmd-hangman-java_1.0.0_all.deb 2>&1 | grep -o 'error: line[^"]*'
error: line 56: Dependency tokens must begin with alpha-numeric, '_' or '/': Requires: default-jre-headless | java17-runtime-headless

The second: even when the dependency is a single name, it is the name from the other system. Build a Java .deb (here named cmd-hangman-jre, so it can sit next to the real one) with only default-jre-headless in it, convert it, and ask Rocky to install the result:

$ cd conv && fpm -s dir -t deb -n cmd-hangman-jre -v 1.0.0 -a all -d default-jre-headless ../cmd-hangman-java=/usr/bin/cmd-hangman-java ../hangman.jar=/usr/share/java/cmd-hangman-java.jar 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman-jre_1.0.0_all.deb"
$ cd conv && fpm -s deb -t rpm cmd-hangman-jre_1.0.0_all.deb 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman-jre-1.0.0-1.noarch.rpm"
$ sudo dnf -y install ./converted.rpm 2>&1 | tail -4
Error:
 Problem: conflicting requests
  - nothing provides default-jre-headless needed by cmd-hangman-jre-1.0.0-1.noarch from @commandline
(try to add '--skip-broken' to skip uninstallable packages or '--nobest' to use not only best candidate packages)

Conversion carries the files across but not the meaning of the dependencies: the converted package asks Rocky for a package that only Debian systems have. Use conversion for packages with no dependencies (or ones named the same on both), and build each format from the source files when they differ, as in the Java section.

Other formats, and what fpm cannot do

The same source files can become other things. A plain tar archive is the simplest (an installer script can unpack it with one command):

$ fpm -t tar -n cmd-hangman cmd-hangman=/usr/bin/cmd-hangman 2>&1 | sed -n 's/.*"Created package", :path=>\(.*\)}/Created package: \1/p'
Created package: "cmd-hangman.tar"
$ tar -tf cmd-hangman.tar
./
./usr/
./usr/bin/
./usr/bin/cmd-hangman

fpm's documentation lists more targets, among them Alpine apk, Arch pacman, macOS pkg and Solaris; they were not tried for this guide, so check fpm --help and its wiki before you rely on one. What fpm cannot do for you, whatever the target:

1. It does not build anything. Compile the program first, as the jar was above; the files must exist.
2. It does not check the package against any distribution's policy. Run lintian and rpmlint, and install on a clean machine, as above.
3. It does not sign a package or publish a repository. Signing and repository steps are the same as in the Build an .rpm and Build a .deb guides: rpmsign, dpkg-scanpackages and createrepo_c work on any package, however it was built.
4. The package knows only what you tell it. A missing -d is a missing dependency, and nobody will notice until a clean machine tries to run it.

fpm or the native tools?

A package is files plus a description. fpm needs the description in command-line options, once, and writes the format; the native tools need it in the format's own files, which takes longer and gives you control over every detail. The comparison:

                               fpm                                        dpkg-deb / rpmbuild
files you write                .fpm file (optional) and scripts          control, copyright, changelog + spec, tarball
one command for both formats   yes (change -t)                           no (two different tools)
learn in an afternoon          yes                                        takes longer
builds from source code        no                                        rpmbuild: yes, in %build
distribution rules and lint    your job; lintian shows gaps              what the files are written for
accepted into Debian/Fedora    no                                        yes, if it follows policy
dependency names               written per command                        written per file
extra tools to install         ruby, a compiler, rpm                     dpkg-deb / rpmbuild only
good for                       internal tools, CI artifacts, customers    packages that go into a distribution

A good rule: if the package is for machines you control, use fpm and spend the time you saved on testing on clean machines. If a distribution will carry it, or you need the source package, use the native tools; the guides Build a .deb Package and Build an .rpm Package show how.

The series so far: Python Game: Build Linux Command Hangman, Build a .deb Package, Build an .rpm Package, Java Game: Build Linux Command Hangman in Java, Package a Java App as a .deb and an .rpm, Side by Side, and this one.