Spawning processes in a portable way in Rust

I’m working on a command-line tool named tool-new-release that automates the release process for the Ember Core Learning Team. It is written in Rust for reasons that I mentioned in “Automating Ember releases with Rust”.

Part of automating the release involves calling the heroku-cli, which I do using std::process::Command. I ran into a small stumbling block, however. Calling the executable directly with std::process:Command was not working when on Windows. So, instead of this:

Comand::new("heroku").arg("auth:whoami");

I had to do this on Windows:

Command::new("cmd").args(&["/C", "heroku", "auth:whoami");

As you can imagine, this raised a bit of a problem. At first I using the cfg macro and two different functions:

pub fn get_env_vars(project: &str) -> Vec<(String, String)> {
    if cfg!(windows) {
        check_heroku_cli_windows();
    } else {
        check_heroku_cli();
    }

    …
}

But this is much too much repetition, so I tried isolating the creating of Command. I couldn’t figure out how, since Command::new('heroku') returns the value struct, and Command::new("cmd").args(&["/C", "heroku"]); returns a mutable Command reference. Then I kept running into lifetime problems, because I was trying to return a value that is owned by the function that creates it, and thus would not live long enough.

Eventually I boiled it down to this:

    let mut cmd = std::process::Command::new("cmd");
    cmd.args(&["/C", "heroku"]);
    cmd

Since I’m returning the value struct, both arms of the cfg! had the same signature, and we were good to go.

As a final optimization, I switched from the std::cfg macro, to the cfg attribute. Now, instead of deciding at run-time which code branch to pick, the compiler does away with the branch it does not need, resulting in a tidier executable.

Putting this all together, we have our final result:

pub fn get_env_vars(project: &str) -> Vec<(String, String)> {
    check_heroku_cli();

    …
}
#[cfg(windows)]
fn heroku_cmd() -> std::process::Command {
    let mut cmd = std::process::Command::new("cmd");
    cmd.args(&["/C", "heroku"]);
    cmd
}

#[cfg(not(windows))]
fn heroku_cmd() -> std::process::Command {
    std::process::Command::new("heroku")
} 

A cool side-effect of using VSCode with rust-analyzer is that when I’m working on the project on my macOS machine, the function that’s marked dwith #[cfg(windows)] is greyed out, since it knows that’s not the target operating system.

You can see the actual PR on tool-new-release‘s repository. If you have any suggestion on how to improve the code base let me know :)

 
1
Kudos
 
1
Kudos

Now read this

Nested components and angle brackets, a sneaky solution

UPDATE With the release of 3.10 you can now use :: for nesting. See bottom of the post. If you have been following Ember development, you might have noticed that starting with Ember v3.4, you have a new way to invoke components in your... Continue →