๐Ÿ“–

Cross compiling a small rust server from MacOS to Raspberry pi

Introduction

I have a small rust web server running on my local network at home, which does nothing fancy but return the password for my wi-fi.

curl raspberrypi.local:3000
{
    "password": "abcd"
}

To manage this, i had to either use a remote window with vscode or just use the terminal and vim or nano. This was hard to manage as i'll regularly see connection timeouts

$ ssh pbaba@raspberrypi.local
// client_loop: send disconnect: Broken pipe

and i didn't have the luxury of my host environment. My package managers, scripts and vscode themes (very important) etc.

Decision

To solve this, i decided to do all my coding on my host machine which is an intel x86_64 mac, package the binary as a debian package and deploy this to my raspberry pi. This will be managed as a systemd service, which will help with managing restarts, new deployments and logging.

Problems

  1. I'll need to package this as a debian package and a bash script to manage deployments. This shouldn't be anything too fancy, it'll use scp to transfer the package.
  2. The raspberrypi is running on ARM, this will mean i'll need to cross-compile from my host machine.
  3. Since this is a rust project, i'll like to manage as much as i can with cargo (Rust's package manager).

Solution

cargo-deb allows us manage debian packages and cross-compile with cargo as a subcommand. Let's install cargo-deb:

cargo install cargo-deb

Running cargo deb should build a debian package for your operating system. The debian package should be found in the /target/debian directory.

$ ls /target/debian
wifi-server_0.1.0-1_amd64.deb

The name is a concatenation of the name, version from the package table in Cargo.toml and the CPU architecture.

[package]
name = "wifi-server"
version = "0.1.0"

Metadata for this package can be managed directly in Cargo.toml under the package.metadata.deb table

[package.metadata.deb]
maintainer = "Paschal Obba <paschalobba@gmail.com>"
copyright = "2024, Paschal Obba"
extended-description = """A server that tells wifi passwords and other details"""
section = "utility"
priority = "optional"
assets = [
    ["target/aarch64-unknown-linux-gnu/release/wifi-server", "/usr/bin/", "755"],
    ["config.toml", "/usr/bin/", "755"]
]

To cross-compile, i needed the toolchain for the raspberry pi. After a few stackoverflow links, i found out that the toolchain for my pi is aarch64-unknown-linux-gnu. I initially thought it was armv7-unknown-linux-gnueabihf but when i ran the executable on my pi i ran into cannot execute: required file not found errors.

We'll need to add this toolchain as a target for rustup.

$ rustup target add aarch64-unknown-linux-gnu
# ............ verify its installed -----
$ rustup target list --installed
aarch64-unknown-linux-gnu # this is the one
armv7-unknown-linux-gnueabihf
wasm32-unknown-unknown
x86_64-apple-darwin 

Using cargo-deb directly for cross compiling was very tedious and this is where cargo-zigbuild which is based off zig comes in. It made the process very seamless.

# firstly install zig if you don't have it
$ brew install zig
# install cargo-zigbuild
$ cargo install cargo-zigbuild
# build for the pi target/toolchain
$ cargo zigbuild --target aarch64-unknown-linux-gnu --release
# build a debian package with cargo-deb
$ cargo deb --target aarch64-unknown-linux-gnu --no-build

After this, we should find that the debian package is now located in /target/aarch64-unknown-linux-gnu/debian and then i send this over ssh to the pi.

$ scp ./target/aarch64-unknown-linux-gnu/debian/wifi-server_0.1.0-1_amd64.deb pbaba@raspberrypi.local:~/server/wifi-server.deb

The raspberrypi.local DNS name is setup using mDNS

Then we can install this package on the pi and run it

# ssh into the pi
$ ssh pbaba@raspberrypi.local
# install debian package
pbaba@raspberrypi $ sudo dpkg -i server/wifi-server.deb
# run it
pbaba@raspberrypi $ wifi-server
# test it
pbaba@raspberrypi $ curl localhost:3000
{
    "password": "abcd"
}

It works! Finally, we'll want to run this as a systemd service. This can be managed with cargo-deb with a few additional keys.

[package.metadata.deb]
# ...
maintainer-scripts = "debian/"
systemd-units = { enable = false }

Since we specified that the scripts are in the debian folder, we'll create a debian folder and a wifi-server.service file.

[Unit]
Description=WI-FI server

[Service]
ExecStart=/usr/bin/wifi-server
Restart=always
RestartSec=1
Environment="RUST_LOG=tower_http=trace"

[Install]
WantedBy=multi-user.target

Now we can manage this with systemctl.

pbaba@raspberrypi:~ $ sudo systemctl enable wifi-server
pbaba@raspberrypi:~ $ sudo systemctl start wifi-server

We can also look at logs for this service with journalctl.

pbaba@raspberrypi:~ $ sudo journalctl -u wifi-server.service
...........
Mar 26 17:53:04 raspberrypi systemd[1]: Started wifi-server.service - WI-FI server.
Mar 26 17:53:15 raspberrypi wifi-server[445450]: 2024-03-26T17:53:15.274862Z DEBUG request{method=GET uri=/ version=HTTP/1.1}: tower_http::trace::on_request: started processing request
Mar 26 17:53:15 raspberrypi wifi-server[445450]: 2024-03-26T17:53:15.274948Z DEBUG request{method=GET uri=/ version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=200

Final Thoughts / References

I should be able to use wasm as a common target and won't need to worry about architectures just incase i want to support more toolchains (i think).