use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use color_eyre::eyre::{Result, WrapErr, eyre};
use fs_err as fs;
use crate::{
CopySpec, SysrootExtract, SysrootKind, SysrootOptions, extract_sysroot, sysroot_copy_options,
};
const LINUX_REQUIRED_COPY_PATHS: &[CopySpec] = &[
CopySpec {
guest_path: "/usr/include",
local_parent: "usr",
},
CopySpec {
guest_path: "/usr/lib",
local_parent: "usr",
},
];
const LINUX_OPTIONAL_COPY_PATHS: &[CopySpec] = &[
CopySpec {
guest_path: "/usr/lib64",
local_parent: "usr",
},
CopySpec {
guest_path: "/lib",
local_parent: "",
},
CopySpec {
guest_path: "/lib64",
local_parent: "",
},
];
const LINUX_LIBRARY_DIRS: &[&str] = &["lib", "lib64", "usr/lib", "usr/lib64"];
const LINUX_LIBC_NAMES: &[&str] = &["libc.so", "libc.so.6", "libc.a"];
const LINUX_REQUIRED_HEADER_FILES: &[&str] = &["usr/include/dirent.h", "usr/include/net/if.h"];
const LINUX_REQUIRED_HEADER_NAMES: &[&str] = &["sys/mount.h"];
pub fn extract_linux_sysroot(options: SysrootOptions) -> Result<PathBuf> {
let SysrootOptions {
root_image,
guest_target,
output_parent,
disk_format,
mount_mode,
force,
} = options;
let dir_name = linux_sysroot_dir_name(&guest_target)?;
let copy_options = sysroot_copy_options(
root_image,
LINUX_REQUIRED_COPY_PATHS,
LINUX_OPTIONAL_COPY_PATHS,
)
.with_disk_format(disk_format)
.with_mount_mode(mount_mode);
extract_sysroot(
SysrootExtract {
kind: SysrootKind::Linux,
output_parent,
dir_name,
copy_options,
force,
},
|_| Ok(()),
|path| validate_linux_sysroot(path),
)
}
pub fn linux_sysroot_dir_name(guest_target: &str) -> Result<String> {
let parts = Vec::from_iter(guest_target.split('-'));
let arch = parts
.first()
.copied()
.filter(|arch| !arch.is_empty())
.ok_or_else(|| eyre!("Linux sysroot target '{guest_target}' is missing an architecture"))?;
let linux_index = parts
.iter()
.position(|part| *part == "linux")
.ok_or_else(|| unsupported_linux_target(guest_target))?;
if parts.len() < 3 {
return Err(unsupported_linux_target(guest_target));
}
match parts
.get(linux_index + 1)
.copied()
.filter(|abi| !abi.is_empty())
{
Some(abi) => Ok(format!("linux-{arch}-{abi}")),
None => Ok(format!("linux-{arch}")),
}
}
fn missing_linux_header_files(sysroot: &Path) -> Result<Vec<String>> {
let mut missing = Vec::from_iter(
LINUX_REQUIRED_HEADER_FILES
.iter()
.filter(|relative| !sysroot.join(relative).is_file())
.map(|relative| (*relative).to_owned()),
);
for header_name in LINUX_REQUIRED_HEADER_NAMES {
if !contains_linux_header_named(sysroot, header_name)? {
missing.push(format!("usr/include/**/{header_name}"));
}
}
Ok(missing)
}
pub fn validate_linux_sysroot(sysroot: impl AsRef<Path>) -> Result<()> {
let sysroot = sysroot.as_ref();
if !sysroot.join("usr/include").is_dir() {
return Err(eyre!(
"Linux sysroot '{}' is missing required directory: usr/include",
sysroot.display()
));
}
let missing_headers = missing_linux_header_files(sysroot)?;
if !missing_headers.is_empty() {
return Err(eyre!(
"Linux sysroot '{}' is missing required libc headers: {}",
sysroot.display(),
missing_headers.join(", ")
));
}
if contains_linux_libc(sysroot)? {
return Ok(());
}
Err(eyre!(
"Linux sysroot '{}' is missing libc in one of: {}",
sysroot.display(),
LINUX_LIBRARY_DIRS.join(", ")
))
}
fn unsupported_linux_target(guest_target: &str) -> color_eyre::Report {
eyre!(
"unsupported Linux sysroot target '{guest_target}'; expected a Rust target triple containing 'linux', for example x86_64-unknown-linux-gnu"
)
}
fn contains_linux_header_named(sysroot: &Path, header_name: &str) -> Result<bool> {
let include_dir = sysroot.join("usr/include");
if !include_dir.is_dir() {
return Ok(false);
}
if include_dir.join(header_name).is_file() {
return Ok(true);
}
for entry in fs::read_dir(&include_dir).wrap_err_with(|| {
format!(
"read Linux sysroot include directory '{}'",
include_dir.display()
)
})? {
let entry = entry?;
if entry.file_type()?.is_dir() && entry.path().join(header_name).is_file() {
return Ok(true);
}
}
Ok(false)
}
fn contains_linux_libc(sysroot: &Path) -> Result<bool> {
for lib_dir in LINUX_LIBRARY_DIRS {
if directory_tree_contains_file_named(&sysroot.join(lib_dir), LINUX_LIBC_NAMES, 3)? {
return Ok(true);
}
}
Ok(false)
}
fn directory_tree_contains_file_named(
root: &Path,
names: &[&str],
max_depth: usize,
) -> Result<bool> {
if !root.is_dir() {
return Ok(false);
}
let mut stack = vec![(root.to_path_buf(), 0)];
while let Some((dir, depth)) = stack.pop() {
for entry in fs::read_dir(&dir)
.wrap_err_with(|| format!("read Linux sysroot library directory '{}'", dir.display()))?
{
let entry = entry?;
let file_name = entry.file_name();
let path = entry.path();
if names.iter().any(|name| file_name == OsStr::new(name)) && path.is_file() {
return Ok(true);
}
if depth < max_depth && entry.file_type()?.is_dir() {
stack.push((path, depth + 1));
}
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linux_sysroot_dir_name_matches_rust_targets() {
assert_eq!(
linux_sysroot_dir_name("x86_64-unknown-linux-gnu").unwrap(),
"linux-x86_64-gnu"
);
assert_eq!(
linux_sysroot_dir_name("aarch64-unknown-linux-musl").unwrap(),
"linux-aarch64-musl"
);
assert_eq!(
linux_sysroot_dir_name("armv7-unknown-linux-gnueabihf").unwrap(),
"linux-armv7-gnueabihf"
);
assert!(linux_sysroot_dir_name("x86_64-unknown-freebsd").is_err());
}
#[test]
fn validates_linux_sysroot_headers_and_libc() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir_all(dir.path().join("usr/include/net"))?;
fs::create_dir_all(dir.path().join("usr/include/sys"))?;
fs::write(dir.path().join("usr/include/dirent.h"), b"dirent")?;
fs::write(dir.path().join("usr/include/net/if.h"), b"if")?;
fs::write(dir.path().join("usr/include/sys/mount.h"), b"mount")?;
fs::create_dir_all(dir.path().join("usr/lib/x86_64-linux-gnu"))?;
fs::write(
dir.path().join("usr/lib/x86_64-linux-gnu/libc.so"),
b"libc linker script",
)?;
validate_linux_sysroot(dir.path())?;
fs::remove_file(dir.path().join("usr/lib/x86_64-linux-gnu/libc.so"))?;
assert!(validate_linux_sysroot(dir.path()).is_err());
Ok(())
}
#[test]
fn rejects_linux_sysroot_missing_init_krun_libc_headers() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir_all(dir.path().join("usr/include"))?;
fs::create_dir_all(dir.path().join("usr/lib64"))?;
fs::write(dir.path().join("usr/lib64/libc.so.6"), b"runtime libc")?;
let error = validate_linux_sysroot(dir.path()).unwrap_err().to_string();
assert!(error.contains("missing required libc headers"));
assert!(error.contains("usr/include/dirent.h"));
assert!(error.contains("usr/include/net/if.h"));
assert!(error.contains("usr/include/**/sys/mount.h"));
Ok(())
}
#[test]
fn accepts_linux_multiarch_include_headers() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir_all(dir.path().join("usr/include/net"))?;
fs::create_dir_all(dir.path().join("usr/include/x86_64-linux-gnu/sys"))?;
fs::create_dir_all(dir.path().join("usr/lib/x86_64-linux-gnu"))?;
fs::write(dir.path().join("usr/include/dirent.h"), b"dirent")?;
fs::write(dir.path().join("usr/include/net/if.h"), b"if")?;
fs::write(
dir.path().join("usr/include/x86_64-linux-gnu/sys/mount.h"),
b"mount",
)?;
fs::write(
dir.path().join("usr/lib/x86_64-linux-gnu/libc.so"),
b"libc linker script",
)?;
validate_linux_sysroot(dir.path())
}
}