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 :)