use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow};
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"];
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(|| {
anyhow!("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}")),
}
}
pub fn validate_linux_sysroot(sysroot: impl AsRef<Path>) -> Result<()> {
let sysroot = sysroot.as_ref();
if !sysroot.join("usr/include").is_dir() {
return Err(anyhow!(
"Linux sysroot '{}' is missing required directory: usr/include",
sysroot.display()
));
}
if contains_linux_libc(sysroot)? {
return Ok(());
}
Err(anyhow!(
"Linux sysroot '{}' is missing libc in one of: {}",
sysroot.display(),
LINUX_LIBRARY_DIRS.join(", ")
))
}
fn unsupported_linux_target(guest_target: &str) -> anyhow::Error {
anyhow!(
"unsupported Linux sysroot target '{guest_target}'; expected a Rust target triple containing 'linux', for example x86_64-unknown-linux-gnu"
)
}
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)
.with_context(|| 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"))?;
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(())
}
}