Writing a Bitrise Step in Rust - The Rustup Setup

Writing a Bitrise Step in Rust - The Rustup Setup

Heads up! An improved version of the step is now available! See here for more details.


Last week I published an article with my experience and some thoughts on rust-script, a tool that allows running simple .rs files as if they were Bash or Python scripts. Learning the underlying implementation was quite interesting, and tools like this make scripting with other programming languages way easier.

In the past few days, however, I also remembered that at Swift Heroes 2022 there was a talk from Atanas Chanev that described how one could write a Bitrise step in the Swift programming language to upload files to the Wasabi Cloud Storage service.

Unfortunately, the talk is not on YouTube yet, but luckily the source code is available at atanas-bitrise/bitrise-wasabi-uploader (also check the run-swift branch). I think this kind of approach paves the way for introducing other programming languages as - unofficial - script languages within Bitrise steps: as a matter of fact, the only supported languages that work out of the box in Bitrise workflows are Bash and Go, but, as we're going to see, this is not gonna stop us from using the language of our choice - Rust.

Installation

First, we need the Bitrise CLI. It's a command line tool that allows running workflows with the YML file we're eventually going to use in the Workflow editor on the Bitrise website, with the exception that running steps locally does not make us consume credits or build time.

The installation is pretty straightforward: the binary is available for download from the GitHub release page and it's as simple as curl-ing and moving it to /usr/local/bin/

curl -fL https://github.com/bitrise-io/bitrise/releases/download/1.49.0/bitrise-$(uname -s)-$(uname -m) > /usr/local/bin/bitrise
Make sure you have the most up-to-date version!

After that, we run bitrise setup to let it download all the additional components it might need ( envman, stepman, analytics plugin and so on) and we're good to go!

Step creation

To create a step, we just have to run bitrise :step create to launch the interactive command that allows specifying all the required details. In our case, we will first create a simple step in Bash to make sure the Rust toolchain is installed.

Creating Step directory at: /home/nick/Developer/rust-playground/bitrise-step-rustup-install
 * [OK] created: /home/nick/Developer/rust-playground/bitrise-step-rustup-install/README.md
 * [OK] created: /home/nick/Developer/rust-playground/bitrise-step-rustup-install/LICENSE
 * [OK] created: /home/nick/Developer/rust-playground/bitrise-step-rustup-install/.gitignore
 * [OK] created: /home/nick/Developer/rust-playground/bitrise-step-rustup-install/step.yml
 * [OK] created: /home/nick/Developer/rust-playground/bitrise-step-rustup-install/bitrise.yml
 * [OK] created: /home/nick/Developer/rust-playground/bitrise-step-rustup-install/.bitrise.secrets.yml
 * [OK] created: /home/nick/Developer/rust-playground/bitrise-step-rustup-install/step.sh

Initializing git repository in step directory ...
 $ git "init"
 $ git "remote" "add" "origin" "https://github.com/nick0602/bitrise-step-rustup-install"

Step is ready!

You can find it at: /home/nick/Developer/rust-playground/bitrise-step-rustup-install

TIP: cd into /home/nick/Developer/rust-playground/bitrise-step-rustup-install and run bitrise run test for a quick test drive!
I cut down a bit on the console output just for convenience

Before jumping in and running our script, let's see what's inside the script folder:

➜  bitrise-step-rustup-install git:(master) ls -la
total 44
drwxr-xr-x  3 nick nick 4096 Aug  1 20:30 .
drwxr-xr-x 17 nick nick 4096 Aug  1 20:33 ..
-rw-r--r--  1 nick nick   41 Aug  1 20:30 .bitrise.secrets.yml
drwxr-xr-x  7 nick nick 4096 Aug  1 20:30 .git
-rw-r--r--  1 nick nick   47 Aug  1 20:30 .gitignore
-rw-r--r--  1 nick nick 1083 Aug  1 20:30 LICENSE
-rw-r--r--  1 nick nick 4410 Aug  1 20:30 README.md
-rw-r--r--  1 nick nick 3772 Aug  1 20:30 bitrise.yml
-rw-r--r--  1 nick nick  939 Aug  1 20:30 step.sh
-rw-r--r--  1 nick nick 3109 Aug  1 20:30 step.yml

The project has a simple structure:

  • We find.bitrise.secret.yml which is a YAML file with key-value pairs defining secrets that can be used within the step
  • A LICENSE file, nothing much to say here, includes a MIT license statement in this case
  • A README.md file we might want to use to describe in a gist what the script does
  • A step.yml file that defines entry points, input and output values of our step
  • A step.sh  - .sh in this case, as we decided to go for Bash - containing the actual script code that will be run
  • A bitrise.yml file containing a test workflow that runs our script. We can see this as some sort of sandbox where we can try things out

If we cd into the step project, we can give it a try:

➜  bitrise-step-rustup-install git:(master) bitrise run test

  ██████╗ ██╗████████╗██████╗ ██╗███████╗███████╗
  ██╔══██╗██║╚══██╔══╝██╔══██╗██║██╔════╝██╔════╝
  ██████╔╝██║   ██║   ██████╔╝██║███████╗█████╗
  ██╔══██╗██║   ██║   ██╔══██╗██║╚════██║██╔══╝
  ██████╔╝██║   ██║   ██║  ██║██║███████║███████╗
  ╚═════╝ ╚═╝   ╚═╝   ╚═╝  ╚═╝╚═╝╚══════╝╚══════╝

  version: 1.49.3

INFO[20:43:20] bitrise runs in Secret Filtering mode
INFO[20:43:20] bitrise runs in Secret Envs Filtering mode
INFO[20:43:20] Running workflow: test

Switching to workflow: test

INFO[20:43:20] Step uses latest version -- Updating StepLib ... 
+------------------------------------------------------------------------------+
| (0) script                                                                   |
+------------------------------------------------------------------------------+
| id: script                                                                   |
| version: 1.2.0                                                               |
| collection: https://github.com/bitrise-io/bitrise-steplib.git                |
| toolkit: bash                                                                |
| time: 2022-08-01T20:43:22+02:00                                              |
+------------------------------------------------------------------------------+
|                                                                              |
Just an example 'secrets' print.
The value of 'A_SECRET_PARAM' is: [REDACTED]
|                                                                              |
+---+---------------------------------------------------------------+----------+
| ✓ | script                                                        | 2.44 sec |
+---+---------------------------------------------------------------+----------+

                                          ▼

+------------------------------------------------------------------------------+
| (1) Switch working dir to test / _tmp dir                                    |
+------------------------------------------------------------------------------+
| id: change-workdir                                                           |
| version: 1.0.3                                                               |
| collection: https://github.com/bitrise-io/bitrise-steplib.git                |
| toolkit: bash                                                                |
| time: 2022-08-01T20:43:23+02:00                                              |
+------------------------------------------------------------------------------+
|                                                                              |
=> Changing working directory to /home/nick/Developer/rust-playground/bitrise-step-rustup-install/_tmp
|                                                                              |
+---+---------------------------------------------------------------+----------+
| ✓ | Switch working dir to test / _tmp dir                         | 0.61 sec |
+---+---------------------------------------------------------------+----------+

                                          ▼

+------------------------------------------------------------------------------+
| (2) Step Test                                                                |
+------------------------------------------------------------------------------+
| id: ./                                                                       |
| version:                                                                     |
| collection: path                                                             |
| toolkit: bash                                                                |
| time: 2022-08-01T20:43:23+02:00                                              |
+------------------------------------------------------------------------------+
|                                                                              |
+ echo 'This is the value specified for the input '\''example_step_input'\'': Example Step Input'\''s value'
This is the value specified for the input 'example_step_input': Example Step Input's value
+ envman add --key EXAMPLE_STEP_OUTPUT --value 'the value you want to share'
|                                                                              |
+---+---------------------------------------------------------------+----------+
| ✓ | Step Test                                                     | 0.06 sec |
+---+---------------------------------------------------------------+----------+

                                          ▼

+------------------------------------------------------------------------------+
| (3) script                                                                   |
+------------------------------------------------------------------------------+
| id: script                                                                   |
| version: 1.2.0                                                               |
| collection: https://github.com/bitrise-io/bitrise-steplib.git                |
| toolkit: bash                                                                |
| time: 2022-08-01T20:43:24+02:00                                              |
+------------------------------------------------------------------------------+
|                                                                              |
This output was generated by the Step (EXAMPLE_STEP_OUTPUT): the value you want to share
|                                                                              |
+---+---------------------------------------------------------------+----------+
| ✓ | script                                                        | 0.64 sec |
+---+---------------------------------------------------------------+----------+


+------------------------------------------------------------------------------+
|                               bitrise summary                                |
+---+---------------------------------------------------------------+----------+
|   | title                                                         | time (s) |
+---+---------------------------------------------------------------+----------+
| ✓ | script                                                        | 2.44 sec |
+---+---------------------------------------------------------------+----------+
| ✓ | Switch working dir to test / _tmp dir                         | 0.61 sec |
+---+---------------------------------------------------------------+----------+
| ✓ | Step Test                                                     | 0.06 sec |
+---+---------------------------------------------------------------+----------+
| ✓ | script                                                        | 0.64 sec |
+---+---------------------------------------------------------------+----------+
| Total runtime: 3.75 sec                                                      |
+------------------------------------------------------------------------------+


Submitting anonymized usage information...
For more information visit:
https://github.com/bitrise-io/bitrise-plugins-analytics/blob/master/README.md

Bitrise build successful

As expected, this workflow does pretty much nothing. Let's get to work!

Setting up step inputs and outputs

Just like any other Bitrise step, we can add input (that will be treated as env variables) that we can use later on to perform stuff in our script, and output variables that can be used in other steps (e.g. some computed value of some sort).

step.yml is the file we're interested in: the setup already generated most of the boilerplate containing the meta information that will be displayed in the Workflow Editor, so let's focus our attention on inputsand outputs.

The default value gives us a rough idea of the required fields:

inputs:
  - example_step_input: Default Value - you can leave this empty if you want to
    opts:
      title: "Example Step Input"
      summary: Summary. No more than 2-3 sentences.
      description: |
        Description of this input.

        Can be Markdown formatted text.
      is_expand: true
      is_required: true
      value_options: []

In our case, we want to allow users to install a nightly version of the Rust toolchain or let them update the existing version (if any) in the container our script is running. We then define RUST_USE_RUSTUP_NIGHTLY, a boolean that defaults to false, and the same goes for RUST_AUTO_UPDATE_TOOLCHAIN:

inputs:
  - RUST_USE_RUSTUP_NIGHTLY: false
    opts:
      title: "Use Rustup Nightly"
      summary: Installs and uses a nightly version of the toolchain
      description: |
        If `true`, forces rustup to run at every run and install the most recent version of the nightly toolchain.
      is_expand: true
      is_required: true
      value_options: [false, true]
  - RUST_AUTO_UPDATE_TOOLCHAIN: false
    opts:
      title: "Update the Rust Toolchain"
      summary: Automatically update the Rust Toolchain at every run.
      description: |
        If `true`, forces the update of the Rust Toolchain.
      is_expand: true
      is_required: true
      value_options: [false, true]      

Moving our attention to the output, usually, we wouldn't need specific values to be exported, but as an exercise, we're going to export CURRENT_RUSTUP_VERSION, CURRENT_RUSTC_VERSION , and CURRENT_CARGO_VERSION respectively for rustup, rustc, and cargo:

outputs:
  - CURRENT_RUSTUP_VERSION:
    opts:
      title: "Current Rust Toolchain version"
      summary: The current Rust Toolchain version
      description: |
        The version returned by the `rustup -V` command.
  - CURRENT_RUSTC_VERSION:
    opts:
      title: "Current Rustc version"
      summary: The current Rustc version
      description: |
        The version returned by the `rustc -V` command.
  - CURRENT_CARGO_VERSION:
    opts:
      title: "Current Cargo version"
      summary: The current Cargo version
      description: |
        The version returned by the `cargo -V` command.

More Bash!

The script that will take care of installing will perform some checks along the way:

  • Check if rustup is installed. If not, download and run the script from the official source as long as RUST_USE_RUSTUP_NIGHTLY is false
  • If rustup is installed, check whether the nightly toolchain has been requested
  • If rustup is installed but on the stable channel, check whether RUST_AUTO_UPDATE_TOOLCHAIN is true to auto-update it
  • Otherwise, if the toolchain is already there, set the default accordingly depending on RUST_USE_RUSTUP_NIGHTLY.

This converts to this Bash snippet:

if ! command -v rustup &> /dev/null && [ "$RUST_USE_RUSTUP_NIGHTLY" = false ]; then
    printf 'No Rust Toolchain found, installing latest stable...\n'
    install_rustup_standard
elif [ "$RUST_USE_RUSTUP_NIGHTLY" = true ]; then
    printf 'Nightly Toolchain required, installing...\n'
    install_rustup_nightly
elif [ "$RUST_AUTO_UPDATE_TOOLCHAIN" = true ]; then
    printf 'Toolchain auto-update enabled, running installation script again...\n'
    install_rustup_standard
fi

# Calling set_default_rustup() to ensure the required version of rustc is used, as ~/.rustup/settings.toml might have been restored from cache.
set_default_rustup

install_rustup_standard and install_rustup_nightly are just functions that proxy the curl from rustup.rs and a rustup toolchain install nightly [...] call, while set_default_rustup sets the proper version of the toolchain if already there.

Once we have installed the toolchain, let's check the versions and export them (remember outputs in the YML file?):

# Export PATH via envman to make the toolchain available for the next steps.
envman add --key "PATH" --value "${PATH}:$HOME/.cargo/bin"

# Use envman to add versions as ENV
envman run rustup -V | envman add --key CURRENT_RUSTUP_VERSION
envman run rustc -V | envman add --key CURRENT_RUSTC_VERSION
envman run cargo -V | envman add --key CURRENT_CARGO_VERSION

# Show current versions
printf "\n\nExported ENV vars:\n"
envman run bash -c printf "CURRENT_RUSTUP_VERSION: $CURRENT_RUSTUP_VERSION"
envman run bash -c printf "CURRENT_RUSTC_VERSION: $CURRENT_RUSTC_VERSION"
envman run bash -c printf "CURRENT_CARGO_VERSION: $CURRENT_CARGO_VERSION"

As seen in the snippet above, here we export via envman the aforementioned versions and make them available for later use. envman run bash -c [...] is actually not required, as it just prints the envs in the log.

Let's commit, push to remote and we're ready to use the step in an actual Bitrise workflow! We can use our custom step even if it's not in the official Step Library, by referencing it with this syntax:

  my_fancy_workflow:
    steps:
    - git::https://github.com/nick0602/bitrise-step-rustup@main:
        title: Rustup Install

For testing purposes, I have created a dummy Bitrise app that does basically nothing, so this is the only step we're going to run for now.

Let's move to the UI Workflow Editor and see what happens, though:

The step is recognized by the editor, and it allows to set the input variables and see what it is actually exporting ( The step will generate these output variables section).

Now let's trigger a build with our workflow and take a look at the logs:

The Rust toolchain is finally available in our workflow and we can build, test, and run any Rust project!

But we're not done yet, as this was just a warm-up. Before moving on to the Rust scripting part, you can find the source of the step shown above here in my GitHub repo.

Niccolò Forlini

Niccolò Forlini

Senior Mobile Engineer