Reviving a 6-Year-Old Rust Project

Posted on 2024-04-06

Introduction

In this post I'll be reviving a Rust project that has been dormant for six years! I'll share the challenges and insights gained while upgrading this project to a modern Rust edition. Whether you're a Rustacean dealing with legacy code or just curious about the evolution of Rust, this post is for you.

The project itself is called rawsort and it's one of the first CLIs I built in Rust that I've used daily. It watches for my camera memory drive being mounted, and then auto imports, sorts, and shapes my image catalog automatically.

Moving from pre-2018 Rust to Rust 2021 Edition

Six years in the programming world is akin to a few decades in other fields. Rust has undergone significant changes in this period. The language has become more mature, with enhancements in safety, performance, and usability. These improvements, while beneficial, introduce a gap between legacy and current code.

First thing to do, is upgrade into the Rust 2021 edition:

# cargo.toml

[package]
# add this
edition = "2021"

name = "rawsort"
version = "0.4.1-alpha.0"
authors = ["Dotan Nahum <jondotan@gmail.com>"]
publish = false

Handling edition errors

First thing to do, is let's drop extern crate instructions. Clippy tells us exactly about this, and offers to fix it as well.

pub mod executor;
mod exif;
extern crate chrono;
extern crate clap;
extern crate dialoguer;
extern crate exif as ext_exif;
extern crate itertools;
extern crate walkdir;
pub mod registry;

into

pub mod executor;
mod exif;
pub mod registry;

Handling actual code errors

Correcting for the evolution of dyn:

error[E0782]: trait objects must include the `dyn` keyword
 --> src/registry.rs:6:39
  |
6 |     map: HashMap<String, (String, Box<Fn(&exif::Exif, &DirEntry) -> String>)>,
  |                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
help: add `dyn` keyword before this trait
  |
6 |     map: HashMap<String, (String, Box<dyn Fn(&exif::Exif, &DirEntry) -> String>)>,
  |                                       +++

And now we're going to solve for the removed externs:

error[E0412]: cannot find type `Exif` in crate `exif`
  --> src/registry.rs:17:22
   |
17 |         T: Fn(&exif::Exif, &DirEntry) -> String + 'static,
   |                      ^^^^ not found in `exif`
   |
help: consider importing this struct
   |
1  + use crate::exif::Exif;
   |
help: if you import `Exif`, refer to it directly
   |
17 -         T: Fn(&exif::Exif, &DirEntry) -> String + 'static,
17 +         T: Fn(&Exif, &DirEntry) -> String + 'static,
   |

Fixing another use statement:

// before
use registry

// after
use crate::registry

Fixing another use rename import issue:

// before
use ext_exif::{Reader, Tag};

// after
use exif::{Reader, Tag};

Here's a prehistoric finding!

error: use of deprecated `try` macro
   --> src/main.rs:224:43
    |
224 |     let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2)));
    |                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: in the 2018 edition `try` is a reserved keyword, and the `try!()` macro is deprecated
help: you can use the `?` operator instead
    |
224 -     let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2)));
224 +     let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?;
    |
help: alternatively, you can still access the deprecated `try!()` macro using the "raw identifier" syntax
    |
224 |     let mut watcher: RecommendedWatcher = r#try!(Watcher::new(tx, Duration::from_secs(2)));
    |                                           ++

Happily refactoring the try! instruction into a ? unwrapping works flawlessly.

That's it other than a few warnings due to some improvements in Clippy suggesting some neater and smarter ways to write Rust, the code is now upgraded.

Handling library breakages

There is still one last error that stems from the slog logging library. Since we upgraded from an old Rust edition, and use and macros semantics changed, we cannot use its macros from the new 2021 edition of Rust.

We'll upgrade slog:

# latest version
slog = { version = "2.7.0" }

This resolves all remaning issues and we can use macros in a more modern way:

// the macros `error`, `info`, `o`
use slog::{error, info, o, Drain, Fuse, Logger};

We're done!

The whole process took less than 10 minutes. And we have a fully-working Rust project, building into a 6 years newer stack!

I challenge you to find any other programming langauge, make upgrades and have this kind of smooth sailing. In terms of programming languages, we just did magic!

Add to that the fact that the Rust authors from six years ago cloud never have predicted what Rust in 2024 will be and so could never imaging how an upgrade experience will be. But, Rust is encouraging a safe and stable mindset all the way from its inception, ensured a safe future for upgrades.

Future-Proofing the Project

Keeping a project up-to-date is a continual effort. But Rust proves that even if you have a very old codebase, you shouldn't be discouraged.

There's very little to be learned here for "future proofing" your code, as most of the changes came from the new Rust editions, however we can have some notes in mind:

  • Breakage in codebase between major language upgrades is guarded by editions and this really works, it's not just theory
  • When a language and ecosystem starts out there's always a "race to logging APIs", and so logging libraries tend to historically break their interfaces a lot
  • There might be a nice place for an "edition tester" CI job, where you can run a codebase on a matrix of different editions, and decide when the amount of upgrades calls for an action

Conclusion

This upgrade journey has been easy. Rust's evolution over these years has been impressive, and adapting to these changes is a safe, encouraging and pain free experience. This only proves yet again, if you want to build software, build it in Rust. If you're an open source maitainer which cares for their time, energy and sanity -- this recommendation is 10x more important. Rust gives you the freedom to not worry about your existing open source project portfolio and not to get buried in maintenance hell.