#![warn(missing_docs)]
use anyhow::Context;
use clap::Parser;
use clap::ValueEnum;
use hvlite_defs::config::DeviceVtl;
use hvlite_defs::config::Hypervisor;
use hvlite_defs::config::PcatBootDevice;
use hvlite_defs::config::VirtioBus;
use hvlite_defs::config::Vtl2BaseAddressType;
use hvlite_defs::config::X2ApicConfig;
use hvlite_defs::config::DEFAULT_PCAT_BOOT_ORDER;
use std::ffi::OsString;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;
#[derive(Parser)]
pub struct Options {
#[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
pub processors: u32,
#[clap(
short = 'm',
long,
value_name = "SIZE",
default_value = "1GB",
value_parser = parse_memory
)]
pub memory: u64,
#[clap(short = 'M', long)]
pub shared_memory: bool,
#[clap(long)]
pub prefetch: bool,
#[clap(short = 'P', long)]
pub paused: bool,
#[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
pub kernel: OptionalPathBuf,
#[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
pub initrd: OptionalPathBuf,
#[clap(short = 'c', long, value_name = "STRING")]
pub cmdline: Vec<String>,
#[clap(long)]
pub hv: bool,
#[clap(long, requires("hv"))]
pub vtl2: bool,
#[clap(long, requires("hv"))]
pub get: bool,
#[clap(long, requires("vtl2"))]
pub no_alias_map: bool,
#[clap(long, requires("vtl2"))]
pub vtl2_emulates_apic: bool,
#[clap(long, requires("vtl2"))]
pub isolation: Option<IsolationCli>,
#[clap(long, value_name = "PATH")]
pub vsock_path: Option<String>,
#[clap(long, value_name = "PATH", requires("vtl2"))]
pub vtl2_vsock_path: Option<String>,
#[clap(long, requires("vtl2"), default_value = "halt")]
pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
#[clap(long)]
pub no_enlightenments: bool,
#[clap(long)]
pub user_mode_apic: bool,
#[clap(long_help = r#"
e.g: --disk memdiff:file:/path/to/disk.vhd
syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
valid disk kinds:
`mem:<len>` memory backed disk
<len>: length of ramdisk, e.g.: `1G`
`memdiff:<disk>` memory backed diff disk
<disk>: lower disk, e.g.: `file:base.img`
`file:\<path\>` file-backed disk
\<path\>: path to file
flags:
`ro` open disk as read-only
`dvd` specifies that device is cd/dvd and it is read_only
`vtl2` assign this disk to VTL2
`uh` relay this disk to VTL0 through Underhill
"#)]
#[clap(long, value_name = "FILE")]
pub disk: Vec<DiskCli>,
#[clap(long_help = r#"
e.g: --nvme memdiff:file:/path/to/disk.vhd
syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
valid disk kinds:
`mem:<len>` memory backed disk
<len>: length of ramdisk, e.g.: `1G`
`memdiff:<disk>` memory backed diff disk
<disk>: lower disk, e.g.: `file:base.img`
`file:\<path\>` file-backed disk
\<path\>: path to file
flags:
`ro` open disk as read-only
`vtl2` assign this disk to VTL2
"#)]
#[clap(long)]
pub nvme: Vec<DiskCli>,
#[clap(long, value_name = "COUNT", default_value = "0")]
pub scsi_sub_channels: u16,
#[clap(long)]
pub nic: bool,
#[clap(long)]
pub net: Vec<NicConfigCli>,
#[clap(long, value_name = "SWITCH_ID")]
pub kernel_vmnic: Vec<String>,
#[clap(long)]
pub gfx: bool,
#[clap(long, requires("vtl2"), conflicts_with("gfx"))]
pub vtl2_gfx: bool,
#[clap(long)]
pub vnc: bool,
#[clap(long, value_name = "PORT", default_value = "5900")]
pub vnc_port: u16,
#[cfg(guest_arch = "x86_64")]
#[clap(long, default_value_t)]
pub apic_id_offset: u32,
#[clap(long)]
pub vps_per_socket: Option<u32>,
#[clap(long, default_value = "auto")]
pub smt: SmtConfigCli,
#[cfg(guest_arch = "x86_64")]
#[clap(long, default_value = "auto", value_parser = parse_x2apic)]
pub x2apic: X2ApicConfig,
#[clap(long)]
pub virtio_console: bool,
#[clap(long, conflicts_with("virtio_console"))]
pub virtio_console_pci: bool,
#[clap(long, value_name = "SERIAL")]
pub com1: Option<SerialConfigCli>,
#[clap(long, value_name = "SERIAL")]
pub com2: Option<SerialConfigCli>,
#[clap(long, value_name = "SERIAL")]
pub com3: Option<SerialConfigCli>,
#[clap(long, value_name = "SERIAL")]
pub com4: Option<SerialConfigCli>,
#[clap(long, value_name = "SERIAL")]
pub virtio_serial: Option<SerialConfigCli>,
#[structopt(long, value_name = "SERIAL")]
pub vmbus_com1_serial: Option<SerialConfigCli>,
#[structopt(long, value_name = "SERIAL")]
pub vmbus_com2_serial: Option<SerialConfigCli>,
#[clap(long, short = 'e')]
pub uefi: bool,
#[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
pub uefi_firmware: OptionalPathBuf,
#[clap(long, requires("uefi"))]
pub uefi_debug: bool,
#[clap(long, requires("uefi"))]
pub uefi_enable_memory_protections: bool,
#[clap(long, requires("pcat"))]
pub pcat_boot_order: Option<PcatBootOrderCli>,
#[clap(long, conflicts_with("uefi"))]
pub pcat: bool,
#[clap(long, requires("pcat"), value_name = "FILE")]
pub pcat_firmware: Option<PathBuf>,
#[clap(long, conflicts_with("kernel"), value_name = "FILE")]
pub igvm: Option<PathBuf>,
#[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
#[clap(long, value_name = "tag:root_path", value_parser = parse_fs_arg)]
pub virtio_9p: Vec<(String, String)>,
#[clap(long)]
pub virtio_9p_debug: bool,
#[clap(long, value_name = "tag:root_path", value_parser = parse_fs_arg)]
pub virtio_fs: Vec<(String, String)>,
#[clap(long, value_name = "tag:root_path", value_parser = parse_fs_arg)]
pub virtio_fs_shmem: Vec<(String, String)>,
#[clap(long, value_name = "BUS", default_value = "auto", value_parser = parse_virtio_bus_arg)]
pub virtio_fs_bus: VirtioBus,
#[clap(long, value_name = "PATH")]
pub virtio_pmem: Option<String>,
#[clap(long)]
pub virtio_net: Vec<NicConfigCli>,
#[clap(long, value_name = "PATH")]
pub log_file: Option<PathBuf>,
#[clap(long, value_name = "SOCKETPATH")]
pub ttrpc: Option<PathBuf>,
#[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
pub grpc: Option<PathBuf>,
#[clap(long)]
pub single_process: bool,
#[cfg(windows)]
#[clap(long, value_name = "PATH")]
pub device: Vec<String>,
#[clap(long, requires("uefi"))]
pub disable_frontpage: bool,
#[clap(long)]
pub tpm: bool,
#[clap(long, default_value = "control", hide(true))]
#[allow(clippy::option_option)]
pub internal_worker: Option<Option<String>>,
#[clap(long)]
pub vmbus_redirect: bool,
#[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
pub vmbus_max_version: Option<u32>,
#[clap(long, value_name = "PATH")]
pub vmgs_file: Option<PathBuf>,
#[clap(long, requires("pcat"), value_name = "FILE")]
pub vga_firmware: Option<PathBuf>,
#[clap(long)]
pub secure_boot: bool,
#[clap(long)]
pub secure_boot_template: Option<SecureBootTemplateCli>,
#[clap(long, value_name = "PATH")]
pub custom_uefi_json: Option<PathBuf>,
#[clap(long, hide(true))]
pub relay_console_path: Option<PathBuf>,
#[clap(long, value_name = "PORT")]
pub gdb: Option<u16>,
#[clap(long)]
pub mana: Vec<NicConfigCli>,
#[clap(long, value_parser = parse_hypervisor)]
pub hypervisor: Option<Hypervisor>,
#[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
pub custom_dsdt: Option<PathBuf>,
#[clap(long_help = r#"
e.g: --ide memdiff:file:/path/to/disk.vhd
syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
valid disk kinds:
`mem:<len>` memory backed disk
<len>: length of ramdisk, e.g.: `1G`
`memdiff:<disk>` memory backed diff disk
<disk>: lower disk, e.g.: `file:base.img`
`file:\<path\>` file-backed disk
\<path\>: path to file
flags:
`ro` open disk as read-only
`s` attach drive to secondary ide channel
`dvd` specifies that device is cd/dvd and it is read_only
"#)]
#[clap(long, value_name = "FILE")]
pub ide: Vec<IdeDiskCli>,
#[clap(long_help = r#"
e.g: --floppy memdiff:/path/to/disk.vfd,ro
syntax: \<path\> | kind:<arg>[,flag,opt=arg,...]
valid disk kinds:
`mem:<len>` memory backed disk
<len>: length of ramdisk, e.g.: `1G`
`memdiff:<disk>` memory backed diff disk
<disk>: lower disk, e.g.: `file:base.img`
`file:\<path\>` file-backed disk
\<path\>: path to file
flags:
`ro` open disk as read-only
"#)]
#[clap(long, value_name = "FILE", requires("pcat"), conflicts_with("uefi"))]
pub floppy: Vec<FloppyDiskCli>,
#[clap(long)]
pub guest_watchdog: bool,
#[clap(long)]
pub underhill_dump_path: Option<PathBuf>,
#[clap(long)]
pub halt_on_reset: bool,
#[clap(long)]
pub write_saved_state_proto: Option<PathBuf>,
#[clap(long)]
pub imc: Option<PathBuf>,
#[clap(long)]
pub mcr: bool, #[clap(long)]
pub battery: bool,
#[clap(long, requires("uefi"))]
pub uefi_console_mode: Option<UefiConsoleModeCli>,
}
fn parse_fs_arg(opt: &str) -> Result<(String, String), &'static str> {
let (tag, root_path) = opt.split_once(':').ok_or("invalid value")?;
Ok((tag.to_owned(), root_path.to_owned()))
}
fn parse_virtio_bus_arg(opt: &str) -> Result<VirtioBus, &'static str> {
Ok(match opt {
"auto" => VirtioBus::Auto,
"mmio" => VirtioBus::Mmio,
"pci" => VirtioBus::Pci,
"vpci" => VirtioBus::Vpci,
_ => return Err("expected one of [auto, mmio, pci, vpci]"),
})
}
#[derive(clap::ValueEnum, Clone, Copy)]
pub enum SecureBootTemplateCli {
Windows,
UefiCa,
}
fn parse_memory(s: &str) -> anyhow::Result<u64> {
|| -> Option<u64> {
let mut b = s.as_bytes();
if s.ends_with('B') {
b = &b[..b.len() - 1]
}
if b.is_empty() {
return None;
}
let multi = match b[b.len() - 1] as char {
'T' => Some(1024 * 1024 * 1024 * 1024),
'G' => Some(1024 * 1024 * 1024),
'M' => Some(1024 * 1024),
'K' => Some(1024),
_ => None,
};
if multi.is_some() {
b = &b[..b.len() - 1]
}
let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
Some(n * multi.unwrap_or(1))
}()
.with_context(|| format!("invalid memory size '{0}'", s))
}
fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
match s.strip_prefix("0x") {
Some(rest) => u64::from_str_radix(rest, 16),
None => s.parse::<u64>(),
}
}
#[derive(Clone)]
pub enum DiskCliKind {
Memory(u64),
MemoryDiff(Box<DiskCliKind>),
PersistentReservationsWrapper(Box<DiskCliKind>),
File(PathBuf),
Blob { kind: BlobKind, url: String },
}
#[derive(Copy, Clone)]
pub enum BlobKind {
Flat,
Vhd1,
}
impl FromStr for DiskCliKind {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let disk = match s.split_once(':') {
None => DiskCliKind::File(PathBuf::from(s)),
Some((kind, arg)) => match kind {
"mem" => DiskCliKind::Memory(parse_memory(arg)?),
"memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
"prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
"file" => DiskCliKind::File(PathBuf::from(arg)),
"blob" => {
let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
let blob_kind = match blob_kind {
"flat" => BlobKind::Flat,
"vhd1" => BlobKind::Vhd1,
_ => anyhow::bail!("unknown blob kind {blob_kind}"),
};
DiskCliKind::Blob {
kind: blob_kind,
url: url.to_string(),
}
}
kind => {
let path_buf = PathBuf::from(s);
if path_buf.has_root() {
DiskCliKind::File(path_buf)
} else {
anyhow::bail!("invalid disk kind {kind}");
}
}
},
};
Ok(disk)
}
}
#[derive(Clone)]
pub struct DiskCli {
pub vtl: DeviceVtl,
pub kind: DiskCliKind,
pub read_only: bool,
pub is_dvd: bool,
pub underhill: Option<UnderhillDiskSource>,
}
#[derive(Copy, Clone)]
pub enum UnderhillDiskSource {
Scsi,
Nvme,
}
impl FromStr for DiskCli {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let mut opts = s.split(',');
let kind = opts.next().unwrap().parse()?;
let mut read_only = false;
let mut is_dvd = false;
let mut underhill = None;
let mut vtl = DeviceVtl::Vtl0;
for opt in opts {
let mut s = opt.split('=');
let opt = s.next().unwrap();
match opt {
"ro" => read_only = true,
"dvd" => {
is_dvd = true;
read_only = true;
}
"vtl2" => {
vtl = DeviceVtl::Vtl2;
}
"uh" => underhill = Some(UnderhillDiskSource::Scsi),
"uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
opt => anyhow::bail!("unknown option: '{opt}'"),
}
}
if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
anyhow::bail!("`uh` is incompatible with `vtl2`");
}
Ok(DiskCli {
vtl,
kind,
read_only,
is_dvd,
underhill,
})
}
}
#[derive(Clone)]
pub struct IdeDiskCli {
pub kind: DiskCliKind,
pub read_only: bool,
pub channel: Option<u8>,
pub device: Option<u8>,
pub is_dvd: bool,
}
impl FromStr for IdeDiskCli {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let mut opts = s.split(',');
let kind = opts.next().unwrap().parse()?;
let mut read_only = false;
let mut channel = None;
let mut device = None;
let mut is_dvd = false;
for opt in opts {
let mut s = opt.split('=');
let opt = s.next().unwrap();
match opt {
"ro" => read_only = true,
"p" => channel = Some(0),
"s" => channel = Some(1),
"0" => device = Some(0),
"1" => device = Some(1),
"dvd" => {
is_dvd = true;
read_only = true;
}
_ => anyhow::bail!("unknown option: '{opt}'"),
}
}
Ok(IdeDiskCli {
kind,
read_only,
channel,
device,
is_dvd,
})
}
}
#[derive(Clone)]
pub struct FloppyDiskCli {
pub kind: DiskCliKind,
pub read_only: bool,
}
impl FromStr for FloppyDiskCli {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let mut opts = s.split(',');
let kind = opts.next().unwrap().parse()?;
let mut read_only = false;
for opt in opts {
let mut s = opt.split('=');
let opt = s.next().unwrap();
match opt {
"ro" => read_only = true,
_ => anyhow::bail!("unknown option: '{opt}'"),
}
}
Ok(FloppyDiskCli { kind, read_only })
}
}
#[derive(Clone)]
pub enum SerialConfigCli {
None,
Console,
NewConsole(Option<PathBuf>),
Stderr,
Pipe(PathBuf),
}
impl FromStr for SerialConfigCli {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let ret = match s {
"none" => SerialConfigCli::None,
"console" => SerialConfigCli::Console,
"stderr" => SerialConfigCli::Stderr,
"term" => SerialConfigCli::NewConsole(None),
s if s.starts_with("term=") => {
SerialConfigCli::NewConsole(Some(PathBuf::from(s.strip_prefix("term=").unwrap())))
}
s if s.starts_with("listen=") => {
SerialConfigCli::Pipe(PathBuf::from(s.strip_prefix("listen=").unwrap()))
}
_ => return Err("invalid serial configuration".into()),
};
Ok(ret)
}
}
#[derive(Clone)]
pub enum EndpointConfigCli {
None,
Consomme { cidr: Option<String> },
Dio { id: Option<String> },
Tap { name: String },
}
impl FromStr for EndpointConfigCli {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
["none"] => EndpointConfigCli::None,
["consomme", s @ ..] => EndpointConfigCli::Consomme {
cidr: s.first().map(|&s| s.to_owned()),
},
["dio", s @ ..] => EndpointConfigCli::Dio {
id: s.first().map(|s| (*s).to_owned()),
},
["tap", name] => EndpointConfigCli::Tap {
name: (*name).to_owned(),
},
_ => return Err("invalid network backend".into()),
};
Ok(ret)
}
}
#[derive(Clone)]
pub struct NicConfigCli {
pub vtl: DeviceVtl,
pub endpoint: EndpointConfigCli,
pub max_queues: Option<u16>,
pub underhill: bool,
}
impl FromStr for NicConfigCli {
type Err = String;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
let mut vtl = DeviceVtl::Vtl0;
let mut max_queues = None;
let mut underhill = false;
while let Some((opt, rest)) = s.split_once(':') {
if let Some((opt, val)) = opt.split_once('=') {
match opt {
"queues" => {
max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
}
_ => break,
}
} else {
match opt {
"vtl2" => {
vtl = DeviceVtl::Vtl2;
}
"uh" => underhill = true,
_ => break,
}
}
s = rest;
}
if underhill && vtl != DeviceVtl::Vtl0 {
return Err("`uh` is incompatible with `vtl2`".into());
}
let endpoint = s.parse()?;
Ok(NicConfigCli {
vtl,
endpoint,
max_queues,
underhill,
})
}
}
#[derive(Debug, Error)]
#[error("unknown hypervisor: {0}")]
pub struct UnknownHypervisor(String);
fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
match s {
"kvm" => Ok(Hypervisor::Kvm),
"mshv" => Ok(Hypervisor::MsHv),
"whp" => Ok(Hypervisor::Whp),
_ => Err(UnknownHypervisor(s.to_owned())),
}
}
#[derive(Debug, Error)]
#[error("unknown VTL2 relocation type: {0}")]
pub struct UnknownVtl2RelocationType(String);
fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
match s {
"disable" => Ok(Vtl2BaseAddressType::File),
s if s.starts_with("auto=") => {
let s = s.strip_prefix("auto=").unwrap_or_default();
let size = if s == "filesize" {
None
} else {
let size = parse_memory(s).map_err(|e| {
UnknownVtl2RelocationType(format!(
"unable to parse memory size from {} for 'auto=' type, {e}",
e
))
})?;
Some(size)
};
Ok(Vtl2BaseAddressType::MemoryLayout { size })
}
s if s.starts_with("absolute=") => {
let s = s.strip_prefix("absolute=");
let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
UnknownVtl2RelocationType(format!(
"unable to parse number from {} for 'absolute=' type",
e
))
})?;
Ok(Vtl2BaseAddressType::Absolute(addr))
}
s if s.starts_with("vtl2=") => {
let s = s.strip_prefix("vtl2=").unwrap_or_default();
let size = if s == "filesize" {
None
} else {
let size = parse_memory(s).map_err(|e| {
UnknownVtl2RelocationType(format!(
"unable to parse memory size from {} for 'vtl2=' type, {e}",
e
))
})?;
Some(size)
};
Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
}
_ => Err(UnknownVtl2RelocationType(s.to_owned())),
}
}
#[derive(Debug, Copy, Clone)]
pub enum SmtConfigCli {
Auto,
Force,
Off,
}
#[derive(Debug, Error)]
#[error("expected auto, force, or off")]
pub struct BadSmtConfig;
impl FromStr for SmtConfigCli {
type Err = BadSmtConfig;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let r = match s {
"auto" => Self::Auto,
"force" => Self::Force,
"off" => Self::Off,
_ => return Err(BadSmtConfig),
};
Ok(r)
}
}
#[cfg_attr(not(guest_arch = "x86_64"), allow(dead_code))]
fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
let r = match s {
"auto" => X2ApicConfig::Auto,
"supported" => X2ApicConfig::Supported,
"off" => X2ApicConfig::Unsupported,
"on" => X2ApicConfig::Enabled,
_ => return Err("expected auto, supported, off, or on"),
};
Ok(r)
}
#[derive(Debug, Copy, Clone, ValueEnum)]
pub enum Vtl0LateMapPolicyCli {
Off,
Log,
Halt,
Exception,
}
#[derive(Debug, Copy, Clone, ValueEnum)]
pub enum IsolationCli {
Vbs,
}
#[derive(Debug, Copy, Clone)]
pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
impl FromStr for PcatBootOrderCli {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
let mut order = Vec::new();
for item in s.split(',') {
let device = match item {
"optical" => PcatBootDevice::Optical,
"hdd" => PcatBootDevice::HardDrive,
"net" => PcatBootDevice::Network,
"floppy" => PcatBootDevice::Floppy,
_ => return Err("unknown boot device type"),
};
let default_pos = default_order
.iter()
.position(|x| x == &Some(device))
.ok_or("cannot pass duplicate boot devices")?;
order.push(default_order[default_pos].take().unwrap());
}
order.extend(default_order.into_iter().flatten());
assert_eq!(order.len(), 4);
Ok(Self(order.try_into().unwrap()))
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum UefiConsoleModeCli {
Default,
Com1,
Com2,
None,
}
fn default_value_from_arch_env(name: &str) -> OsString {
let prefix = if cfg!(guest_arch = "x86_64") {
"X86_64"
} else if cfg!(guest_arch = "aarch64") {
"AARCH64"
} else {
return Default::default();
};
let prefixed = format!("{}_{}", prefix, name);
std::env::var_os(name)
.or_else(|| std::env::var_os(prefixed))
.unwrap_or_default()
}
#[derive(Clone)]
pub struct OptionalPathBuf(pub Option<PathBuf>);
impl From<&std::ffi::OsStr> for OptionalPathBuf {
fn from(s: &std::ffi::OsStr) -> Self {
OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
}
}