Simplifying Debian Packaging for Rust: A Step-by-Step Guide for Rust Developers

In the rapidly evolving world of software development, efficiency and simplicity are key. Packaging your applications for Debian-based systems can be a crucial step for those working with Rust, a powerful language known for its safety and performance. Today, I’m excited to guide you through converting your Rust package into a Debian (.deb) package that can later be installed on Debian, Ubuntu, Linux Mint, Pop_OS, and other Debian-based distros. This tutorial is designed to be straightforward, quick, and easy to follow.

Jan Bronicki
Rust Programming Language

--

Starting with a Basic Rust Project

Our journey begins with a simple “Hello World” project in Rust. This project is created using Cargo, the Rust package manager, and features a clean Cargo.toml file with no dependencies:

$ cargo new my-app --bin

$ tree
.
|-- Cargo.toml
`-- src
`-- main.rs

$ cat Cargo.toml
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"

[dependencies]

Preparing Your Project for Packaging

Before diving into the packaging process, it's essential to ensure that your project is functional and you are on a Debian-based system so that you can access Debian packaging-specific commands. If you are not on a Debian-based system you can simply open your project in a docker container and build it there:

docker run -it -v <path-to-your-project>:<where-to-mount-in-docker> debian:latest 

This is the exact command I ran, it mounts my project directory in /app directory of the docker container:

docker run -it -v ~/Documents/Programming/debpkging/my-app:/app debian:latest 

Once confirmed, you can begin the transformation into a Debian package. This first involves installing a Cargo plugin called cargo-deb:

cargo install cargo-deb

Configuring Cargo.toml

The next step is crucial — updating the Cargo.toml file with the necessary metadata for your Debian package. This metadata includes details like the maintainer, copyright, etc.:

[package]
name = "my-app"
version = "0.1.0"
edition = "2021"

[dependencies]

[package.metadata.deb]
maintainer = "Jan Bronicki <janbronicki@gmail.com>"
copyright = "2024, Jan Bronicki <janbronicki@gmail.com>"
extended-description = """A simple hello world program!"""
depends = "$auto"
section = "utility"
priority = "optional"
assets = [
["target/release/my-app", "usr/bin/", "755"],
]

You can find out more about available fields here.

Specifying Binary Output in Cargo.toml

The most critical aspect of this process is instructing cargo-deb on handling the output binary. When you build your Rust project using Cargo, it outputs to the target/directory, with subdirectories for release and debug versions. In our case, working on a simple binary project, the output is my-app binary.

Here’s where the specifics come in. You need to tell cargo-deb to take this binary and place it in a specific directory upon installation of the package. This is done by specifying a path in the metadata. For example, you might want your binary to reside in /usr/bin/ after installation. However, in the configuration, you don’t include the ‘root’ ( / ) part of the path. This might seem confusing at first, but it’s a crucial step in ensuring your binary is correctly located.

assets = [
["target/release/my-app", "usr/bin/", "755"],
]

Here I would like to mention that where you should put your binaries has some logic behind it, for the sake of the simplicity of this guide we are going to put them in /usr/bin/ but you should research beforehand where your binaries should be put on a production system, here is a great article that outlines that.

Building Your .deb Package

With your project configured, the next step is to run cargo deb to build the .deb package:

$ cargo deb
warning: description field is missing in Cargo.toml
warning: license field is missing in Cargo.toml
Finished release [optimized] target(s) in 0.00s
/app/target/debian/my-app_0.1.0-1_arm64.deb

Installing and Testing the Package

After building the package, it’s time to install and test it. This involves using the dpkg -i command to install the package and then execute it to ensure it works correctly. After building the package you should see the path to the .deb file as part of the output itself, you can simply use that path to now install the package with dpkg in my case it's going to be:

$ dpkg -i ./target/debian/my-app_0.1.0-1_arm64.deb
Selecting previously unselected package my-app.
(Reading database ... 16606 files and directories currently installed.)
Preparing to unpack .../my-app_0.1.0-1_arm64.deb ...
Unpacking my-app (0.1.0-1) ...
Setting up my-app (0.1.0-1) ...

Verifying and Uninstalling the Package

Finally, verify that the package is correctly installed and learn how to uninstall it. This is crucial for maintaining the integrity of your system.

After installing the package the my-app binary should be visible and executable from any place on the system:

$ my-app
Hello, world!

You can also check that the system is aware that this is an installed package by running dpkg --list:

$ dpkg --list | grep my-app
ii my-app 0.1.0-1 arm64 [generated from Rust crate my-app]

We can also see that the my-app binary resides in the specified directory in Cargo.toml file:

$ ls /usr/bin/ | grep my-app
my-app

If you wish to uninstall the package you can use the dpkg command again:

$ dpkg -r my-app
(Reading database ... 16609 files and directories currently installed.)
Removing my-app (0.1.0-1) ...

This will uninstall the package from the local package registry as well as delete all files belonging to the package, so you can rest assured that you will not leave any trash behind:

$ my-app
bash: /usr/bin/my-app: No such file or directory
$ ls /usr/bin/ | grep my-app
$ # As you can see above the my-app binary was not found

Conclusion

Packaging a Rust application for Debian systems is not only limited to Rust; this technique can be adapted for various programming languages. It’s a valuable skill that enhances your development workflow, allowing you to distribute your applications more efficiently.

What’s next?

The journey doesn’t stop here. This approach is not just limited to simple applications but can also be extended to libraries and more significant projects. Integration into your CI/CD pipeline can automate the packaging process, making distribution and version management a breeze. Whether you add your package to the official Debian repositories or host your own Personal Package Archive (PPA), these steps provide a foundation for broader distribution and easier access for users.

I hope you found this guide helpful. Remember to share, and follow for more content like this!

This article is based on this video:

--

--

Jan Bronicki
Rust Programming Language

Tech and open-source enthusiast 💻, Engineer 👷‍♂️, Python Developer 🐍, Rusteacean 🦀