Reproducible Shell Toolbox

Core Technology

The key to this entire setup is rtx, which is a clone of asdf in Rust. Both of these projects are runtime version managers, similar to something like rustup for Rust, virtual environments for Python, and sdkman for JVM languages.

What's special about asdf is that its built entirely with shell scripts, and was developed in such a way as to be usable for nearly anything you'd want to get multiple different versions of. My general understanding is that it works through something called shims instead of directly manipulating the PATH environment variable. Rtx instead directly manipulates the PATH, and it does so every time the shell prompt repaints itself.

Here's a breakdown of the script I use to set everything up, the latest version of which can be found in my dotfiles.

Step 0: Install Chezmoi

This might come out of the blue, but the first thing I need to do is install chezmoi, which is the tool I use to manage my dotfiles. My dotfiles contain my configuration files for all the tools I use, including rtx, so it's the first thing I need to get my hands on.

It can be installed with a one-liner that looks like this:

sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply gitlab.com/dkrautha

After this I delete the chezmoi executable that the one-liner downloaded and instead install it through rtx.

Step 1: Install rtx

Rtx has a statically compiled binary available for Linux (and probably other platforms too), which you can get with the following:

curl https://rtx.pub/install.sh | sh

Because I run this in a shell script, I also add the following lines to make sure I can use rtx while in this script:

eval "$(~/.local/share/rtx/bin/rtx activate -s bash)"
eval "$(rtx hook-env)"

Once this is done we can now start installing rtx plugins.

Step 2: Install Plugins

I have two asdf plugins that I maintain for myself, one for mold and one for chafa. Normally when you search for available plugins with rtx or asdf it'll return a list of the plugins that match a query. I am not sure how to get my plugins to show up in this list, so as a result I need to install them manually from a URL:

rtx install mold https://github.com/dkrautha/asdf-mold
rtx install chafa https://github.com/dkrautha/asdf-chafa

After this I use the following to install the plugins I have defined in my rtx config:

rtx install

My config is located at ~/.config/rtx/config.toml and contains something similar to the following:

[tools]
neovim = "stable"
rust = "stable"
chezmoi = "latest"
java = "latest"
go = "latest"
node = "20"
kotlin = "1.8"
gradle = "8"
fzf = "latest"
zola = "latest"
poetry = "latest"
cmake = "3.26"
meson = "1.1"
ninja = "1.11"
mold = "1.11"
chafa = "1.12"

[settings]
experimental = true

This config file is for globally used plugins, but if you look into rtx or asdf more there are ways to override which plugins and what versions of those plugins are meant to be used while in a particular folder.

Before moving on I also re-initialize plugins within this script to make sure that Rust will show up, so I can install the rest of my cli tools with cargo:

eval "$(rtx hook-env)"

Step 3: Install Rust Programs

For installing programs with cargo I use cargo-binstall, which I must first install regularly with cargo:

cargo install cargo-binstall

After this I then install my tools of choice:

cargo binstall -y \
	b3sum \
	bacon \
	bat \
	bottom \
	broot \
	cargo-binstall \
	cargo-docs \
	cargo-tarpaulin \
	cargo-update \
	exa \
	fd-find \
	hexyl \
	hyperfine \
	just \
	kondo \
	names \
	procs \
	ripgrep \
	sccache \
	sd \
	starship \
	tokei \
	topgrade \
	trashy \
	type_buddy \
	watchexec-cli \
	zellij \
	zoxide

Step 4: Install Projectdo

You can find projectdo here. Its main purpose is to provide single letter commands for common project actions. For example, b will be for building, r for running, and t for testing the project. There's support for a number of different build systems, but the one that matters most to me is Cargo for Rust.

I install the latest version from the master branch with:

curl https://raw.githubusercontent.com/paldepind/projectdo/master/projectdo -o ~/.local/bin/projectdo
chmod +x ~/.local/bin/projectdo

Step 5: Install Fish Plugins

I use a couple of different plugins with my shell of choice, fish. I install the fisher package manager and the plugins listed in my config with the following:

if [ -x /usr/bin/fish ]; then
	fish -c "\
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish \
| source && fisher install jorgebucaran/fisher
"
fi

This only installs fisher if fish is executable, that way if I'm stuck with bash on a particular machine or in a container it won't try and fail.

Measuring Elapsed Time in C and Rust

C: <time.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

enum { v_size = 100000000 };

void long_function() {
  double* v = malloc(v_size * sizeof(double));

  if (!v) {
    fprintf(stderr, "Memory allocation failed!\n");
    exit(1);
  }

  // initializing array to all 55's
  for (int i = 0; i < v_size; i += 1) {
    v[i] = 55;
  }

  // performing the transformation
  for (int i = 0; i < v_size; i += 1) {
    v[i] += 1;
  }

  free(v);
}

int main() {
  // current time measured in seconds relative to an epoch
  time_t start_calendar = time(0);

  // raw processor clock time since the program started
  clock_t start_clock = clock();

  // new in C11, returns calendar time in seconds and nanoseconds based on a
  // given time base
  struct timespec start_timespec = {0};
  timespec_get(&start_timespec, TIME_UTC); // this function has an error return
                                           // type, ignoring it for brevity

  long_function();

  time_t end_calendar = time(0);
  clock_t end_clock = clock();
  struct timespec end_timespec = {0};
  timespec_get(&end_timespec, TIME_UTC);

  // computes the difference between two time_t calendar time in seconds
  double calendar_diff = difftime(end_calendar, start_calendar);
  // difference between two clock_t using CLOCKS_PER_SEC
  double clock_diff = 1.0 * (end_clock - start_clock) / CLOCKS_PER_SEC;
  // difference between two timespecs
  double timespec_sec_diff =
      difftime(end_timespec.tv_sec, start_timespec.tv_sec);
  long timespec_nanosec_diff = end_timespec.tv_nsec - start_timespec.tv_nsec;

  printf("calendar_diff: %lf\nclock_diff: %lf\ntimespec_sec_diff: "
         "%lf\ntimespec_nanosec_diff: %ld",
         calendar_diff, clock_diff, timespec_sec_diff, timespec_nanosec_diff);
}

Rust: std::time

use std::time::{Duration, Instant, SystemTime};

// Function which presumably takes a while to execute
fn long_function() {
    let mut v = vec![55; 100000000];
    v.iter_mut().for_each(|x| *x += 1);
}

fn main() {
    // A system clock which uses your operating systems current time
    // This can go backwards!
    let start_system = SystemTime::now();

    // A monotonic non-decreasing clock, i.e. a clock that should always increase
    // by the same amount, and never go backwards.
    let start_instant = Instant::now();

    long_function();

    // Because the system clock is susceptible to drift and could
    // go backwards, this operation returns a result.
    let duration_system = start_system.elapsed().unwrap();

    // there is no possibility of this occurring with durations from Instants
    let duration_instant = start_instant.elapsed();

    println!(
        "Time elapsed in long function: \nSystemTime: {:?}\nInstant: {:?}",
        // defaults to milliseconds if an as_period() is not used
        duration_system,
        // converts to floating point seconds, see the docs for other
        // possible as_*() methods
        duration_instant.as_secs_f32()
    );
}