use std::ffi::CString;
use std::io;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use super::super::{ensure_parent_dirs_scoped, ScopedMutationTarget};
use super::private_temp::Binding;
use super::swiftpm::{shell_quote, write_adapter_file};
pub(super) fn prepare(root: &Path, swift: &Path, binding: &Binding) -> io::Result<Option<PathBuf>> {
let compiler = if swift.parent() == Some(Path::new("/usr/bin")) {
selected_system_compiler(root, binding)?
} else {
swift.with_file_name("swiftc")
};
let frontend = compiler.with_file_name("swift");
if !compiler.is_file() || !frontend.is_file() {
return Ok(None);
}
let source = format!(
"#!/bin/sh\n# Generated by Harn's confined SwiftPM adapter.\nexport TMPDIR={} TMP={} TEMP={} DIRHELPER_USER_DIR_SUFFIX={}\nexec {} \"$@\"\n",
shell_quote(&binding.directory.to_string_lossy()),
shell_quote(&binding.directory.to_string_lossy()),
shell_quote(&binding.directory.to_string_lossy()),
shell_quote(&binding.suffix),
shell_quote(&compiler.to_string_lossy()),
);
let identity = harn_kernel::pure::sha256_hex(source.as_bytes());
let relative = PathBuf::from(".harn-toolchain-cache/swiftpm/toolsets").join(identity);
write_adapter_file(
&ScopedMutationTarget {
root: root.to_path_buf(),
relative: relative.join("usr/bin/swiftc"),
},
source.as_bytes(),
)?;
publish_link(
&ScopedMutationTarget {
root: root.to_path_buf(),
relative: relative.join("usr/bin/swift"),
},
&frontend,
)?;
if let Some(lib) = compiler
.parent()
.and_then(Path::parent)
.map(|path| path.join("lib"))
.filter(|path| path.is_dir())
{
publish_link(
&ScopedMutationTarget {
root: root.to_path_buf(),
relative: relative.join("usr/lib"),
},
&lib,
)?;
}
let path = root.join(&relative).join("toolset.json");
let json = serde_json::to_vec(&serde_json::json!({
"schemaVersion": "1.0",
"swiftCompiler": {"path": root.join(&relative).join("usr/bin/swiftc")},
}))?;
write_adapter_file(
&ScopedMutationTarget {
root: root.to_path_buf(),
relative: relative.join("toolset.json"),
},
&json,
)?;
Ok(Some(path))
}
fn selected_system_compiler(root: &Path, binding: &Binding) -> io::Result<PathBuf> {
let cache = ScopedMutationTarget {
root: root.to_path_buf(),
relative: PathBuf::from(".harn-toolchain-cache/xcrun/xcrun_db"),
};
let _ = ensure_parent_dirs_scoped(&cache)?;
let mut command = super::super::command_for::xcode_compiler_query();
let output = command
.current_dir(root)
.env("TMPDIR", &binding.directory)
.env("xcrun_db", root.join(&cache.relative))
.output()?;
if !output.status.success() {
return Err(io::Error::other(format!(
"cannot resolve selected Swift compiler: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let path = PathBuf::from(
String::from_utf8(output.stdout)
.map_err(io::Error::other)?
.trim(),
);
if !path.is_absolute() || !path.is_file() {
return Err(io::Error::other(
"xcrun returned no installed Swift compiler",
));
}
Ok(path)
}
fn publish_link(target: &ScopedMutationTarget, source: &Path) -> io::Result<()> {
let (parent, name) = ensure_parent_dirs_scoped(target)?;
let name = CString::new(name).map_err(io::Error::other)?;
let source = CString::new(source.as_os_str().as_bytes()).map_err(io::Error::other)?;
let result = unsafe { libc::symlinkat(source.as_ptr(), parent.as_raw_fd(), name.as_ptr()) };
if result == 0 {
return Ok(());
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::AlreadyExists {
return Err(error);
}
let mut existing = vec![0u8; source.as_bytes().len() + 1];
let count = unsafe {
libc::readlinkat(
parent.as_raw_fd(),
name.as_ptr(),
existing.as_mut_ptr().cast(),
existing.len(),
)
};
if count < 0 {
return Err(io::Error::last_os_error());
}
if &existing[..count as usize] != source.as_bytes() {
return Err(io::Error::other(
"compiler projection has a different target",
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compiler_projection_is_exclusive_and_refuses_redirected_parents() {
let workspace = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let target = ScopedMutationTarget {
root: workspace.path().to_path_buf(),
relative: PathBuf::from("toolset/usr/bin/swift"),
};
let compiler = Path::new("/installed/toolchain/swift");
publish_link(&target, compiler).unwrap();
publish_link(&target, compiler).unwrap();
assert!(publish_link(&target, Path::new("/different/swift")).is_err());
assert_eq!(
std::fs::read_link(target.root.join(&target.relative)).unwrap(),
compiler
);
let occupied = ScopedMutationTarget {
root: workspace.path().to_path_buf(),
relative: PathBuf::from("occupied"),
};
std::fs::write(occupied.root.join(&occupied.relative), "unchanged").unwrap();
assert!(publish_link(&occupied, compiler).is_err());
assert_eq!(
std::fs::read_to_string(occupied.root.join(&occupied.relative)).unwrap(),
"unchanged"
);
std::os::unix::fs::symlink(outside.path(), workspace.path().join("redirected")).unwrap();
let redirected = ScopedMutationTarget {
root: workspace.path().to_path_buf(),
relative: PathBuf::from("redirected/swift"),
};
assert!(publish_link(&redirected, compiler).is_err());
assert_eq!(std::fs::read_dir(outside.path()).unwrap().count(), 0);
}
}