Demilade Sonuga's blog
All postsAutomating The Build Process
I'm sure that by now, you would have gotten tired of typing in these commands to run the game:
cargo build
sudo qemu-system-x86_64 \
-drive if=pflash,format=raw,unit=0,file=/path/to/OVMF_CODE.fd,readonly=on \
-drive if=pflash,unit=1,format=raw,file=/path/to/OVMF_VARS.fd \
-drive format=raw,file=fat:rw:target/x86_64-unknown-uefi/debug/
You might already have some way of avoiding the need to type this out every time, maybe a shell script or a simple copy-and-paste scheme.
What we're going to do now is to make this process easier for ourselves. We want running this project to be as
easy as just typing in cargo run
and hitting enter. Because of the no_std
nature of this project, we won't
be able to use cargo run
(as is), since cargo run
attempts to run the project with the OS you're developing
with.
Rather than trying to hack our way with cargo run
, we'll create a utility crate that will do the heavy lifting
for us.
This utility is going to be called runner
and our aim is that cargo runner
should build and run the Blasterball
project. Before we get on to that, we're first going to need to do some directory restructuring.
At this point, the directory looks like this:
blasterball/
| .git/ // Hidden, if you're using git
| .cargo/
| | config.toml
| src/
| | bitmap.rs
| | block.bmp
| | font.rs
| | main.rs
| | uefi.rs
| .gitignore
| Cargo.lock
| Cargo.toml
We're going to change it to this:
blasterball-project/
| .git/ // Hidden, if you're using git
| .cargo/
| | config.toml
| blasterball/
| | src/
| | | bitmap.rs
| | | block.bmp
| | | font.rs
| | | main.rs
| | | uefi.rs
| | Cargo.lock
| | Cargo.toml
| runner/
| | src/
| | | main.rs
| | Cargo.toml
| .gitignore
| Cargo.lock
| Cargo.toml
The first thing you need to do is create a new directory blasterball-project
outside your blasterball
directory.
mkdir blasterball-project
Now move over your blasterball
directory into blasterball-project
.
mv blasterball blasterball-project
If you're using git, you have to move .git/
out of blasterball/
into
blasterball-project/
.
Assuming your current directory is the blasterball-project
directory:
mv blasterball/.git .
Now, you have to let Git know of your changes (Again, only if you're using git).
Assuming your current directory is the blasterball-project
directory:
git add .
git commit -m "Moved the blasterball crate into the blasterball-project workspace"
In your blasterball-project
directory, run
cargo new --bin runner
Now you have two binaries in blasterball-project
, the runner
which we'll use for running the game
and the game itself, blasterball
.
In the same blasterball-project
directory, create a new Cargo.toml
file and throw in the following:
[workspace]
members = [
"blasterball",
"runner"
]
This is just to indicate to cargo
that the two binaries belong to the same workspace. A workspace in
Rust is just a way of organizing code such that multiple crates are logically grouped together.
In the blasterball-project
directory, create a new .cargo/config.toml
and add:
[alias]
runner = "run --package runner"
Now, whenever cargo runner
is run in the blasterball-project/
directory, the runner
crate will be built
and executed.
Try running cargo runner
now and you should have "Hello, world!" printed out.
Open your runner
's main.rs
file and you'll see:
fn main() {
println!("Hello, world!");
}
which is the default code in a cargo-initialized project.
When this runner
is run, it ought to do the following:
- Build
blasterball
- Run
blasterball
We start from step 1. To build blasterball
, we first need a way of invoking cargo build
on it from
the code. The standard library offers utilities we can use for that in std::process
.
Before we continue, at the beginning of this blog series, it was said that the only crates we'll be using to
build the game are core
and compiler_builtins
. This runner
is not the game, but rather a utility that
we're just going to use to run the game in an emulator, so using the standard library here is not a problem.
The std::process::Command
struct provides us with the basic stuff we need to invoke other programs, like
cargo
. To check it out, throw this into runner
's main.rs
:
use std::process::Command;
fn main() {
let output = Command::new("ls")
.arg("-l")
.output()
.unwrap();
println!("{}", std::str::from_utf8(&output.stdout).unwrap());
}
This imports the standard library's Command
struct and uses it to invoke the ls
command which lists
the items in the current directory. Running it should give you something like this:
total 8
drwxr-xr-x. 1 demilade demilade 70 Dec 22 14:47 blasterball
-rw-r--r--. 1 demilade demilade 202 Dec 23 13:45 Cargo.lock
-rw-r--r--. 1 demilade demilade 57 Dec 23 13:45 Cargo.toml
drwxr-xr-x. 1 demilade demilade 26 Dec 22 14:47 runner
drwxr-xr-x. 1 demilade demilade 118 Dec 22 14:31 target
Okay, now that we know how to start another process from our code, we just run
cargo build --package blasterball
with the Command
struct's mechanism to build the game.
One more thing we need to note is this:
[build]
target = "x86_64-unknown-uefi"
[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]
These are the contents of blasterball
's .cargo/config.toml
file. The configuration in this file will
no longer be taking effect. You can delete the file now. Instead of configuring blasterball
's build with
the config file, we'll be passing it directly to the cargo build
invocation with the
--target x86_64-unknown-uefi
and -Zbuild-std=core
options.
In runner
's main.rs
:
/* DELETED:
fn main() {
let output = Command::new("ls")
.arg("-l")
.output()
.unwrap();
println!("{}", std::str::from_utf8(&output.stdout).unwrap());
}
*/
fn main() {
let status = Command::new("cargo")
.arg("build")
.arg("--package")
.arg("blasterball")
.arg("-Z")
.arg("build-std=core")
.arg("--target")
.arg("x86_64-unknown-uefi")
.status()
.unwrap();
if !status.success() {
eprintln!("Failed to build the game");
std::process::exit(1);
}
}
The above is equivalent to cargo build --package blasterball -Zbuild-std=core --target x86_64-unknown-uefi
.
The status
which is invoked after the arg declarations executes the command and returns the exit status.
If the status says it failed, an error message is printed to standard error and execution stops here.
We're done with step 1. Our game runner now builds the game. All that's left is to run it. Running it by hand, the command looks like this:
sudo qemu-system-x86_64 \
-drive if=pflash,format=raw,unit=0,file=/path/to/OVMF_CODE.fd,readonly=on \
-drive if=pflash,unit=1,format=raw,file=/path/to/OVMF_VARS.fd \
-drive format=raw,file=fat:rw:target/x86_64-unknown-uefi/debug/
In our runner's main
function:
// NEW:
const OVMF_PATH: &'static str = "/path/to/ovmf/";
fn main() {
let status = Command::new("cargo")
.arg("build")
.arg("--package")
.arg("blasterball")
.arg("-Z")
.arg("build-std=core")
.arg("--target")
.arg("x86_64-unknown-uefi")
.status()
.unwrap();
if !status.success() {
eprintln!("Failed to build the game");
std::process::exit(1);
}
// NEW:
let status = Command::new("sudo")
.arg("qemu-system-x86_64")
.arg("-drive")
.arg(&format!("if=pflash,format=raw,unit=0,file={}/OVMF_CODE.fd,readonly=on", OVMF_PATH))
.arg("-drive")
.arg(&format!("if=pflash,unit=1,format=raw,file={}/OVMF_VARS.fd", OVMF_PATH))
.arg("-drive")
.arg("format=raw,file=fat:rw:target/x86_64-unknown-uefi/debug/")
.status()
.unwrap();
if !status.success() {
eprintln!("Failed to start emulator");
std::process::exit(1);
}
}
The translation to Rust is very straightforward due to Command
's well-designed API. The OVMF_PATH
at
the top there should be replaced with whatever it is on your own computer. On mine, it's /usr/share/edk2/ovmf/
.
If you're not already familiar with it, the format!
macro takes arguments like println!
's but returns
a string instead of printing to the terminal.
Running cargo runner
in the top-level directory now will go through the process of building the game
and invoking the emulator.
Take Away
- The
Command
struct in the standard library is used for creating new processes.
Your directory should be looking like this now:
blasterball-project/
| .git/ // Hidden, if you're using git
| .cargo/
| | config.toml
| blasterball/
| | src/
| | | bitmap.rs
| | | block.bmp
| | | font.rs
| | | main.rs
| | | uefi.rs
| | Cargo.lock
| | Cargo.toml
| runner/
| | src/
| | | main.rs
| | Cargo.toml
| .gitignore
| Cargo.lock
| Cargo.toml
For the full code, go to the repo
In The Next Post
We'll get on to animating bitmaps
References
- https://doc.rust-lang.org/std/process/struct.Command.html