use std::io::{self, Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::orchestration::CapabilityPolicy;
use super::super::{
ensure_parent_dirs_scoped, normalized_workspace_roots, openat_file, renameat_name,
unlinkat_name, warn_once, ScopedMutationTarget,
};
const ADAPTER_ROOT: &str = ".harn-toolchain-cache/swiftpm";
const SUBCOMMANDS: &[&str] = &["build", "test", "run", "package"];
const XCRUN_VALUE_OPTIONS: &[&str] = &["--sdk", "--toolchain"];
const XCRUN_SWITCHES: &[&str] = &[
"-v",
"--verbose",
"-l",
"--log",
"-r",
"--run",
"-n",
"--no-cache",
"-k",
"--kill-cache",
];
struct DefaultOption {
name: &'static str,
value: Option<&'static str>,
}
const DEFAULTS: &[DefaultOption] = &[
DefaultOption {
name: "--disable-sandbox",
value: None,
},
DefaultOption {
name: "--manifest-cache",
value: Some("local"),
},
DefaultOption {
name: "--cache-path",
value: Some(".build/harn/swiftpm/cache"),
},
DefaultOption {
name: "--config-path",
value: Some(".build/harn/swiftpm/config"),
},
DefaultOption {
name: "--security-path",
value: Some(".build/harn/swiftpm/security"),
},
];
pub(super) fn compatible_args(program: &str, args: &[String]) -> Vec<String> {
let Some(offset) = subcommand_offset(program, args) else {
return args.to_vec();
};
let rest = &args[offset + 1..];
let mut rewritten = args[..=offset].to_vec();
for option in DEFAULTS {
if !has_option(rest, option.name) {
rewritten.push(option.name.to_string());
rewritten.extend(option.value.map(str::to_string));
}
}
rewritten.extend_from_slice(rest);
rewritten
}
fn subcommand_offset(program: &str, args: &[String]) -> Option<usize> {
let mut offset = 0;
match Path::new(program).file_name()?.to_str()? {
"swift" => {}
"xcrun" => {
while let Some(arg) = args.get(offset) {
if XCRUN_VALUE_OPTIONS.contains(&arg.as_str()) {
offset += 2;
} else if XCRUN_SWITCHES.contains(&arg.as_str())
|| XCRUN_VALUE_OPTIONS
.iter()
.any(|name| arg.starts_with(&format!("{name}=")))
{
offset += 1;
} else {
break;
}
}
if args.get(offset).map(String::as_str) != Some("swift") {
return None;
}
offset += 1;
}
_ => return None,
}
args.get(offset)
.filter(|arg| SUBCOMMANDS.contains(&arg.as_str()))
.map(|_| offset)
}
pub(super) fn has_option(args: &[String], option: &str) -> bool {
args.iter()
.take_while(|arg| arg.as_str() != "--")
.any(|arg| arg == option || arg.starts_with(&format!("{option}=")))
}
fn shell_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}
fn launcher_source(name: &str, real_program: &Path) -> String {
let mut source = format!(
"#!/bin/bash\n# Generated by Harn's SwiftPM sandbox adapter.\nset -e\nreal={}\nargs=(\"$@\")\noffset=0\n",
shell_quote(&real_program.to_string_lossy())
);
if name == "xcrun" {
source.push_str(&format!(
"while (( offset < ${{#args[@]}} )); do\n case \"${{args[offset]}}\" in\n {}) ((offset += 2));;\n {}) ((offset += 1));;\n *) break;;\n esac\ndone\n[[ \"${{args[offset]-}}\" == swift ]] || exec \"$real\" \"${{args[@]}}\"\n((offset += 1))\n",
XCRUN_VALUE_OPTIONS.join("|"),
XCRUN_SWITCHES.iter().map(|item| item.to_string())
.chain(XCRUN_VALUE_OPTIONS.iter().map(|item| format!("{item}=*")))
.collect::<Vec<_>>().join("|")
));
}
source.push_str(&format!(
"case \"${{args[offset]-}}\" in\n {}) ;;\n *) exec \"$real\" \"${{args[@]}}\";;\nesac\nrest=(\"${{args[@]:offset+1}}\")\ndefaults=()\n",
SUBCOMMANDS.join("|")
));
for option in DEFAULTS {
source.push_str(&format!(
"present=0\nfor arg in \"${{rest[@]}}\"; do\n [[ \"$arg\" == -- ]] && break\n case \"$arg\" in {}|{}=*) present=1; break;; esac\ndone\nif (( !present )); then defaults+=({}{}); fi\n",
option.name, option.name, shell_quote(option.name),
option.value.map(|value| format!(" {}", shell_quote(value))).unwrap_or_default()
));
}
source.push_str("exec \"$real\" \"${args[@]:0:offset+1}\" \"${defaults[@]}\" \"${rest[@]}\"\n");
source
}
pub(in crate::stdlib::sandbox) fn inject_env(
env: &mut Vec<(String, String)>,
policy: &CapabilityPolicy,
) {
if !policy.sandbox_profile.confines_processes()
|| super::super::effective_fallback(policy.sandbox_profile)
== super::super::SandboxFallback::Off
|| !super::super::active_backend_available()
{
return;
}
let Some(root) = normalized_workspace_roots(policy).into_iter().next() else {
return;
};
let inherited = env
.iter()
.find(|(key, _)| key == "PATH")
.map(|(_, value)| value.clone())
.or_else(
|| match crate::stdlib::process::current_session_environment() {
Some(environment) => environment.launcher_value("PATH").map(str::to_string),
None => crate::test_env::env_var_seamed("PATH"),
},
);
let Some(inherited) = inherited.filter(|path| !path.is_empty()) else {
return;
};
match adapted_path(&root, &inherited) {
Ok(Some(path)) => {
env.retain(|(key, _)| key != "PATH");
env.push(("PATH".to_string(), path));
}
Ok(None) => {}
Err(error) => warn_once(
"handler_sandbox_swiftpm_adapter",
&format!("could not prepare confined SwiftPM launchers: {error}; retaining the original tool search path"),
),
}
}
fn adapted_path(root: &Path, inherited: &str) -> io::Result<Option<String>> {
let adapter_root = root.join(ADAPTER_ROOT);
let original_dirs = std::env::split_paths(inherited)
.filter(|path| !path.starts_with(&adapter_root))
.collect::<Vec<_>>();
let original_path = std::env::join_paths(&original_dirs)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
let original_env = Some(vec![(
"PATH".to_string(),
original_path.to_string_lossy().into_owned(),
)]);
let mut launchers = Vec::new();
for name in ["swift", "xcrun"] {
let real = crate::stdlib::process::resolve_program_path(name, &original_env, true, &[]);
if real == name {
continue;
}
let real = PathBuf::from(real);
let real = if real.is_absolute() {
real
} else {
std::env::current_dir()?.join(real)
};
launchers.push((name, launcher_source(name, &real)));
}
if launchers.is_empty() {
return Ok(None);
}
let identity = launchers
.iter()
.map(|(name, source)| format!("{name}\0{source}\0"))
.collect::<String>();
let relative_dir =
Path::new(ADAPTER_ROOT).join(harn_kernel::pure::sha256_hex(identity.as_bytes()));
for (name, source) in launchers {
write_launcher(
&ScopedMutationTarget {
root: root.to_path_buf(),
relative: relative_dir.join(name),
},
source.as_bytes(),
)?;
}
let dirs = std::iter::once(root.join(relative_dir)).chain(original_dirs);
std::env::join_paths(dirs)
.map(|path| Some(path.to_string_lossy().into_owned()))
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))
}
fn write_launcher(target: &ScopedMutationTarget, source: &[u8]) -> io::Result<()> {
let (parent, name) = ensure_parent_dirs_scoped(target)?;
let parent_fd = parent.as_raw_fd();
if let Ok(file) = openat_file(
parent_fd,
&name,
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK,
0,
) {
let metadata = file.metadata()?;
if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
let mut existing = Vec::new();
file.take(source.len() as u64 + 1)
.read_to_end(&mut existing)?;
if existing == source {
return Ok(());
}
}
}
static NEXT_FILE: AtomicU64 = AtomicU64::new(0);
let temporary = format!(
".{name}.{}.{}.tmp",
std::process::id(),
NEXT_FILE.fetch_add(1, Ordering::Relaxed)
);
let mut file = openat_file(
parent_fd,
&temporary,
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o700,
)?;
let result = (|| {
file.set_permissions(std::fs::Permissions::from_mode(0o700))?;
file.write_all(source)?;
file.flush()?;
renameat_name(parent_fd, &temporary, parent_fd, &name)
})();
if result.is_err() {
let _ = unlinkat_name(parent_fd, &temporary, 0);
}
result
}
#[cfg(test)]
#[path = "swiftpm_tests.rs"]
mod tests;