Skip to content

Commit 4221e42

Browse files
QEMU/OVMF improvements (#474)
Improvements to the `cargo xtask run` command to be more robust across different operating systems.
1 parent 8833ff9 commit 4221e42

File tree

4 files changed

+219
-57
lines changed

4 files changed

+219
-57
lines changed

.github/workflows/rust.yml

+3-6
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ jobs:
2323
sudo add-apt-repository ppa:canonical-server/server-backports
2424
sudo apt-get update
2525
sudo apt-get install qemu-system-arm qemu-efi-aarch64 -y
26-
# Copy the files so that the vars file isn't read-only.
27-
cp /usr/share/AAVMF/AAVMF_CODE.fd uefi-test-runner/QEMU_EFI-pflash.raw
28-
cp /usr/share/AAVMF/AAVMF_VARS.fd uefi-test-runner/vars-template-pflash.raw
2926
3027
- name: Install stable
3128
uses: actions-rs/toolchain@v1
@@ -102,8 +99,8 @@ jobs:
10299
# guard against external changes breaking the CI.
103100
EDK2_NIGHTLY_COMMIT: 'ebb83e5475d49418afc32857f66111949928bcdc'
104101
run: |
105-
curl -o uefi-test-runner/OVMF32_CODE.fd https://raw.githubusercontent.com/retrage/edk2-nightly/${EDK2_NIGHTLY_COMMIT}/bin/RELEASEIa32_OVMF_CODE.fd
106-
curl -o uefi-test-runner/OVMF32_VARS.fd https://raw.githubusercontent.com/retrage/edk2-nightly/${EDK2_NIGHTLY_COMMIT}/bin/RELEASEIa32_OVMF_VARS.fd
102+
curl -o OVMF32_CODE.fd https://raw.githubusercontent.com/retrage/edk2-nightly/${EDK2_NIGHTLY_COMMIT}/bin/RELEASEIa32_OVMF_CODE.fd
103+
curl -o OVMF32_VARS.fd https://raw.githubusercontent.com/retrage/edk2-nightly/${EDK2_NIGHTLY_COMMIT}/bin/RELEASEIa32_OVMF_VARS.fd
107104
108105
- name: Install stable
109106
uses: actions-rs/toolchain@v1
@@ -121,7 +118,7 @@ jobs:
121118
run: cargo xtask build --target ia32
122119

123120
- name: Run VM tests
124-
run: cargo xtask run --target ia32 --headless --ci
121+
run: cargo xtask run --target ia32 --headless --ci --ovmf-code OVMF32_CODE.fd --ovmf-vars OVMF32_VARS.fd
125122
timeout-minutes: 2
126123

127124
test:

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ Available commands:
8787
Especially useful if you want to run the tests under
8888
[WSL](https://docs.microsoft.com/en-us/windows/wsl) on Windows.
8989
- `--headless`: run QEMU without a GUI
90-
- `--ovmf-dir <PATH>`: directory in which to look for OVMF files
90+
- `--ovmf-code <PATH>`: path of an OVMF code file
91+
- `--ovmf-vars <PATH>`: path of an OVMF vars file
9192
- `--release`: build in release mode
9293
- `--target {x86_64,ia32,aarch64}`: choose target UEFI arch
9394
- `test`: run unit tests and doctests on the host

xtask/src/opt.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,13 @@ pub struct QemuOpt {
140140
#[clap(long, action)]
141141
pub headless: bool,
142142

143-
/// Directory in which to look for OVMF files.
143+
/// Path of an OVMF code file.
144144
#[clap(long, action)]
145-
pub ovmf_dir: Option<PathBuf>,
145+
pub ovmf_code: Option<PathBuf>,
146+
147+
/// Path of an OVMF vars file.
148+
#[clap(long, action)]
149+
pub ovmf_vars: Option<PathBuf>,
146150
}
147151

148152
/// Build uefi-test-runner and run it in QEMU.

xtask/src/qemu.rs

+208-48
Original file line numberDiff line numberDiff line change
@@ -11,83 +11,224 @@ use std::ffi::OsString;
1111
use std::io::{BufRead, BufReader, Read, Write};
1212
use std::path::{Path, PathBuf};
1313
use std::process::{Child, Command, Stdio};
14-
use std::thread;
14+
use std::{env, thread};
1515
use tempfile::TempDir;
1616

17+
#[derive(Clone, Copy, Debug)]
18+
enum OvmfFileType {
19+
Code,
20+
Vars,
21+
}
22+
23+
impl OvmfFileType {
24+
fn as_str(&self) -> &'static str {
25+
match self {
26+
Self::Code => "code",
27+
Self::Vars => "vars",
28+
}
29+
}
30+
}
31+
1732
struct OvmfPaths {
1833
code: PathBuf,
1934
vars: PathBuf,
20-
vars_read_only: bool,
2135
}
2236

2337
impl OvmfPaths {
24-
fn from_dir(dir: &Path, arch: UefiArch) -> Self {
38+
fn get_path(&self, file_type: OvmfFileType) -> &Path {
39+
match file_type {
40+
OvmfFileType::Code => &self.code,
41+
OvmfFileType::Vars => &self.vars,
42+
}
43+
}
44+
45+
/// Get the Arch Linux OVMF paths for the given guest arch.
46+
fn arch_linux(arch: UefiArch) -> Self {
2547
match arch {
48+
// Package "edk2-armvirt".
2649
UefiArch::AArch64 => Self {
27-
code: dir.join("QEMU_EFI-pflash.raw"),
28-
vars: dir.join("vars-template-pflash.raw"),
29-
// The OVMF implementation for AArch64 won't boot unless
30-
// the vars file is writeable.
31-
vars_read_only: false,
50+
code: "/usr/share/edk2-armvirt/aarch64/QEMU_CODE.fd".into(),
51+
vars: "/usr/share/edk2-armvirt/aarch64/QEMU_VARS.fd".into(),
3252
},
53+
// Package "edk2-ovmf".
3354
UefiArch::IA32 => Self {
34-
code: dir.join("OVMF32_CODE.fd"),
35-
vars: dir.join("OVMF32_VARS.fd"),
36-
vars_read_only: true,
55+
code: "/usr/share/edk2-ovmf/ia32/OVMF_CODE.fd".into(),
56+
vars: "/usr/share/edk2-ovmf/ia32/OVMF_VARS.fd".into(),
3757
},
58+
// Package "edk2-ovmf".
3859
UefiArch::X86_64 => Self {
39-
code: dir.join("OVMF_CODE.fd"),
40-
vars: dir.join("OVMF_VARS.fd"),
41-
vars_read_only: true,
60+
code: "/usr/share/edk2-ovmf/x64/OVMF_CODE.fd".into(),
61+
vars: "/usr/share/edk2-ovmf/x64/OVMF_VARS.fd".into(),
4262
},
4363
}
4464
}
4565

46-
fn exists(&self) -> bool {
47-
self.code.exists() && self.vars.exists()
66+
/// Get the CentOS OVMF paths for the given guest arch.
67+
fn centos_linux(arch: UefiArch) -> Option<Self> {
68+
match arch {
69+
// Package "edk2-aarch64".
70+
UefiArch::AArch64 => Some(Self {
71+
code: "/usr/share/edk2/aarch64/QEMU_EFI-pflash.raw".into(),
72+
vars: "/usr/share/edk2/aarch64/vars-template-pflash.raw".into(),
73+
}),
74+
// There's no official ia32 package.
75+
UefiArch::IA32 => None,
76+
// Package "edk2-ovmf".
77+
UefiArch::X86_64 => Some(Self {
78+
// Use the `.secboot` variant because the CentOS package
79+
// doesn't have a plain "OVMF_CODE.fd".
80+
code: "/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd".into(),
81+
vars: "/usr/share/edk2/ovmf/OVMF_VARS.fd".into(),
82+
}),
83+
}
4884
}
4985

50-
/// Find path to OVMF files.
51-
fn find(opt: &QemuOpt, arch: UefiArch) -> Result<Self> {
52-
// If the path is specified in the settings, use it.
53-
if let Some(ovmf_dir) = &opt.ovmf_dir {
54-
let ovmf_paths = Self::from_dir(ovmf_dir, arch);
55-
if ovmf_paths.exists() {
56-
return Ok(ovmf_paths);
57-
}
58-
bail!("OVMF files not found in {}", ovmf_dir.display());
86+
/// Get the Debian OVMF paths for the given guest arch. These paths
87+
/// also work on Ubuntu.
88+
fn debian_linux(arch: UefiArch) -> Self {
89+
match arch {
90+
// Package "qemu-efi-aarch64".
91+
UefiArch::AArch64 => Self {
92+
code: "/usr/share/AAVMF/AAVMF_CODE.fd".into(),
93+
vars: "/usr/share/AAVMF/AAVMF_VARS.fd".into(),
94+
},
95+
// Package "ovmf-ia32".
96+
UefiArch::IA32 => Self {
97+
code: "/usr/share/OVMF/OVMF32_CODE_4M.secboot.fd".into(),
98+
vars: "/usr/share/OVMF/OVMF32_VARS_4M.fd".into(),
99+
},
100+
// Package "ovmf".
101+
UefiArch::X86_64 => Self {
102+
code: "/usr/share/OVMF/OVMF_CODE.fd".into(),
103+
vars: "/usr/share/OVMF/OVMF_VARS.fd".into(),
104+
},
105+
}
106+
}
107+
108+
/// Get the Fedora OVMF paths for the given guest arch.
109+
fn fedora_linux(arch: UefiArch) -> Self {
110+
match arch {
111+
// Package "edk2-aarch64".
112+
UefiArch::AArch64 => Self {
113+
code: "/usr/share/edk2/aarch64/QEMU_EFI-pflash.raw".into(),
114+
vars: "/usr/share/edk2/aarch64/vars-template-pflash.raw".into(),
115+
},
116+
// Package "edk2-ovmf-ia32".
117+
UefiArch::IA32 => Self {
118+
code: "/usr/share/edk2/ovmf-ia32/OVMF_CODE.fd".into(),
119+
vars: "/usr/share/edk2/ovmf-ia32/OVMF_VARS.fd".into(),
120+
},
121+
// Package "edk2-ovmf".
122+
UefiArch::X86_64 => Self {
123+
code: "/usr/share/edk2/ovmf/OVMF_CODE.fd".into(),
124+
vars: "/usr/share/edk2/ovmf/OVMF_VARS.fd".into(),
125+
},
59126
}
127+
}
60128

61-
// Check whether the test runner directory contains the files.
62-
let ovmf_dir = Path::new("uefi-test-runner");
63-
let ovmf_paths = Self::from_dir(ovmf_dir, arch);
64-
if ovmf_paths.exists() {
65-
return Ok(ovmf_paths);
129+
/// Get the Windows OVMF paths for the given guest arch.
130+
fn windows(arch: UefiArch) -> Self {
131+
match arch {
132+
UefiArch::AArch64 => Self {
133+
code: r"C:\Program Files\qemu\share\edk2-aarch64-code.fd".into(),
134+
vars: r"C:\Program Files\qemu\share\edk2-arm-vars.fd".into(),
135+
},
136+
UefiArch::IA32 => Self {
137+
code: r"C:\Program Files\qemu\share\edk2-i386-code.fd".into(),
138+
vars: r"C:\Program Files\qemu\share\edk2-i386-vars.fd".into(),
139+
},
140+
UefiArch::X86_64 => Self {
141+
code: r"C:\Program Files\qemu\share\edk2-x86_64-code.fd".into(),
142+
// There's no x86_64 vars file, but the i386 one works.
143+
vars: r"C:\Program Files\qemu\share\edk2-i386-vars.fd".into(),
144+
},
66145
}
146+
}
67147

148+
/// Get candidate paths where OVMF code/vars might exist for the
149+
/// given guest arch and host platform.
150+
fn get_candidate_paths(arch: UefiArch) -> Vec<Self> {
151+
let mut candidates = Vec::new();
68152
if platform::is_linux() {
69-
let possible_paths = [
70-
// Most distros, including CentOS, Fedora, Debian, and Ubuntu.
71-
Path::new("/usr/share/OVMF"),
72-
// Arch Linux.
73-
Path::new("/usr/share/ovmf/x64"),
74-
];
75-
for path in possible_paths {
76-
let ovmf_paths = Self::from_dir(path, arch);
77-
if ovmf_paths.exists() {
78-
return Ok(ovmf_paths);
153+
candidates.push(Self::arch_linux(arch));
154+
if let Some(candidate) = Self::centos_linux(arch) {
155+
candidates.push(candidate);
156+
}
157+
candidates.push(Self::debian_linux(arch));
158+
candidates.push(Self::fedora_linux(arch));
159+
}
160+
if platform::is_windows() {
161+
candidates.push(Self::windows(arch));
162+
}
163+
candidates
164+
}
165+
166+
/// Search for an OVMF file (either code or vars).
167+
///
168+
/// If `user_provided_path` is not None, it is always used. An error
169+
/// is returned if the path does not exist.
170+
///
171+
/// Otherwise, the paths in `candidates` are searched to find one
172+
/// that exists. If none of them exist, an error is returned.
173+
fn find_ovmf_file(
174+
file_type: OvmfFileType,
175+
user_provided_path: &Option<PathBuf>,
176+
candidates: &[Self],
177+
) -> Result<PathBuf> {
178+
if let Some(path) = user_provided_path {
179+
// The user provided an exact path to use; verify that it
180+
// exists.
181+
if path.exists() {
182+
Ok(path.to_owned())
183+
} else {
184+
bail!(
185+
"ovmf {} file does not exist: {}",
186+
file_type.as_str(),
187+
path.display()
188+
);
189+
}
190+
} else {
191+
for candidate in candidates {
192+
let path = candidate.get_path(file_type);
193+
if path.exists() {
194+
return Ok(path.to_owned());
79195
}
80196
}
197+
198+
bail!(
199+
"no ovmf {} file found in candidates: {:?}",
200+
file_type.as_str(),
201+
candidates
202+
.iter()
203+
.map(|c| c.get_path(file_type))
204+
.collect::<Vec<_>>(),
205+
);
81206
}
207+
}
82208

83-
bail!("OVMF files not found anywhere");
209+
/// Find path to OVMF files.
210+
fn find(opt: &QemuOpt, arch: UefiArch) -> Result<Self> {
211+
let candidates = Self::get_candidate_paths(arch);
212+
213+
let code = Self::find_ovmf_file(OvmfFileType::Code, &opt.ovmf_code, &candidates)?;
214+
let vars = Self::find_ovmf_file(OvmfFileType::Vars, &opt.ovmf_vars, &candidates)?;
215+
216+
Ok(Self { code, vars })
84217
}
85218
}
86219

87-
fn add_pflash_args(cmd: &mut Command, file: &Path, read_only: bool) {
220+
enum PflashMode {
221+
ReadOnly,
222+
ReadWrite,
223+
}
224+
225+
fn add_pflash_args(cmd: &mut Command, file: &Path, mode: PflashMode) {
88226
// Build the argument as an OsString to avoid requiring a UTF-8 path.
89227
let mut arg = OsString::from("if=pflash,format=raw,readonly=");
90-
arg.push(if read_only { "on" } else { "off" });
228+
arg.push(match mode {
229+
PflashMode::ReadOnly => "on",
230+
PflashMode::ReadWrite => "off",
231+
});
91232
arg.push(",file=");
92233
arg.push(file);
93234

@@ -263,6 +404,18 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
263404
};
264405
let mut cmd = Command::new(qemu_exe);
265406

407+
if platform::is_windows() {
408+
// The QEMU installer for Windows does not automatically add the
409+
// directory containing the QEMU executables to the PATH. Add
410+
// the default directory to the PATH to make it more likely that
411+
// QEMU will be found on Windows. (The directory is appended, so
412+
// if a different directory on the PATH already has the QEMU
413+
// binary this change won't affect anything.)
414+
let mut path = env::var_os("PATH").unwrap_or_default();
415+
path.push(r";C:\Program Files\qemu");
416+
cmd.env("PATH", path);
417+
}
418+
266419
// Disable default devices.
267420
// QEMU by defaults enables a ton of devices which slow down boot.
268421
cmd.arg("-nodefaults");
@@ -313,10 +466,20 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
313466
}
314467
}
315468

469+
let tmp_dir = TempDir::new()?;
470+
let tmp_dir = tmp_dir.path();
471+
316472
// Set up OVMF.
317473
let ovmf_paths = OvmfPaths::find(opt, arch)?;
318-
add_pflash_args(&mut cmd, &ovmf_paths.code, /*read_only=*/ true);
319-
add_pflash_args(&mut cmd, &ovmf_paths.vars, ovmf_paths.vars_read_only);
474+
475+
// Make a copy of the OVMF vars file so that it can be used
476+
// read+write without modifying the original. Under AArch64, some
477+
// versions of OVMF won't boot if the vars file isn't writeable.
478+
let ovmf_vars = tmp_dir.join("ovmf_vars");
479+
fs_err::copy(&ovmf_paths.vars, &ovmf_vars)?;
480+
481+
add_pflash_args(&mut cmd, &ovmf_paths.code, PflashMode::ReadOnly);
482+
add_pflash_args(&mut cmd, &ovmf_vars, PflashMode::ReadWrite);
320483

321484
// Mount a local directory as a FAT partition.
322485
cmd.arg("-drive");
@@ -331,9 +494,6 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
331494
cmd.args(&["-display", "none"]);
332495
}
333496

334-
let tmp_dir = TempDir::new()?;
335-
let tmp_dir = tmp_dir.path();
336-
337497
let test_disk = tmp_dir.join("test_disk.fat.img");
338498
create_mbr_test_disk(&test_disk)?;
339499

0 commit comments

Comments
 (0)