harn-vm 0.10.167

Async bytecode virtual machine for the Harn programming language
Documentation
//! Preserve the private temp binding when Swift Build clears a compiler's env.
//!
//! A SwiftPM toolset selects a launcher for the same installed compiler. Its
//! adjacent frontend and libraries remain read-only projections of that
//! toolchain, including libSwiftScan used by the integrated driver.

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)?;
    // SAFETY: both C strings are live and parent owns the opened directory.
    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];
    // SAFETY: the directory and C name are live, and the buffer is writable.
    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);
    }
}