rllvm 0.2.0

A tool to build whole-program LLVM bitcode files
Documentation
//! Marker objects and archive patching for the rustc wrapper.
//!
//! rustc gives rllvm two injection points and no third: `-C link-arg` for
//! crate types that link, and the finished archive for crate types that do
//! not. Both end up carrying the same crate-level bitcode path.

use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
};

use crate::{config::try_rllvm_config, error::Error, utils::embed_bitcode_filepath_to_object_file};

/// Compile an empty translation unit and embed the bitcode path into it.
///
/// The object goes to rustc as `-C link-arg`, so the linker concatenates its
/// section along with every other object's, and `rllvm-get-bc` finds the path
/// in the finished binary.
///
/// Compiled rather than synthesised with the `object` crate on purpose: a
/// synthesised Mach-O drops the platform load command, which makes the linker
/// warn about every object rllvm touches. Compiled rather than assembled from
/// a `.s` because that would need a section directive per object format.
pub(crate) fn build_marker_object(bitcode: &Path, dir: &Path) -> Result<PathBuf, Error> {
    let source = dir.join("rllvm_marker.c");
    // A C translation unit may not be empty, and the declaration must not
    // define a symbol that could collide at link time.
    fs::write(
        &source,
        b"typedef int rllvm_marker_empty_translation_unit;\n",
    )?;

    let marker = dir.join("rllvm_marker.o");
    let clang = try_rllvm_config()?.clang_filepath().clone();
    let status = Command::new(&clang)
        .arg("-c")
        .arg(&source)
        .arg("-o")
        .arg(&marker)
        .status()?;
    if !status.success() {
        return Err(Error::ExecutionFailure(format!(
            "Failed to build the rllvm marker object with {clang:?}: exit_status={status}"
        )));
    }

    embed_bitcode_filepath_to_object_file::<&Path>(bitcode, &marker, None)?;

    Ok(marker)
}

/// Embed the bitcode path into every object member of an archive.
///
/// rustc archives rlibs and staticlibs itself, so there is no link to
/// intercept. Every member carries the same crate-level path, which is
/// correct: the `.bc` is per crate, not per codegen unit, so whichever member
/// the linker pulls in contributes the whole crate.
///
/// Returns the number of members patched.
pub(crate) fn patch_archive(archive: &Path, bitcode: &Path) -> Result<usize, Error> {
    let llvm_ar = try_rllvm_config()?.llvm_ar_filepath().clone();
    let archive = archive.canonicalize()?;

    let workspace = tempfile::tempdir()?;
    let status = Command::new(&llvm_ar)
        .arg("x")
        .arg(&archive)
        .current_dir(workspace.path())
        .status()?;
    if !status.success() {
        return Err(Error::ExecutionFailure(format!(
            "Failed to unpack {archive:?} with {llvm_ar:?}: exit_status={status}"
        )));
    }

    let mut patched = Vec::new();
    for entry in fs::read_dir(workspace.path())? {
        let member = entry?.path();
        // An rlib carries `lib.rmeta` and `lib.rmeta-link` beside its objects.
        let data = fs::read(&member)?;
        if object::File::parse(&*data).is_err() {
            continue;
        }
        embed_bitcode_filepath_to_object_file::<&Path>(bitcode, &member, None)?;
        patched.push(member);
    }

    if patched.is_empty() {
        tracing::debug!("No object members to patch in {archive:?}");
        return Ok(0);
    }

    // `r` replaces the named members and leaves every other one, and its
    // ordering, alone -- rustc still has to be able to read the rlib.
    let status = Command::new(&llvm_ar)
        .arg("r")
        .arg(&archive)
        .args(&patched)
        .status()?;
    if !status.success() {
        return Err(Error::ExecutionFailure(format!(
            "Failed to repack {archive:?} with {llvm_ar:?}: exit_status={status}"
        )));
    }

    Ok(patched.len())
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::utils::{
        extract_bitcode_filepaths_from_object_file, extract_bitcode_filepaths_from_parsed_objects,
    };

    /// A placeholder bitcode file. Embedding canonicalizes the path, so the
    /// file has to exist, but nothing ever reads its contents.
    fn placeholder_bitcode(dir: &Path) -> PathBuf {
        let bitcode = dir.join("crate.bc");
        fs::write(&bitcode, b"placeholder").expect("failed to write the placeholder bitcode");
        bitcode
    }

    /// An archive shaped like an rlib: object members plus one that is not an
    /// object, which must be left alone.
    fn build_fixture_archive(dir: &Path, objects: usize) -> PathBuf {
        let config = try_rllvm_config().expect("no usable LLVM configuration");
        let clang = config.clang_filepath().clone();
        let llvm_ar = config.llvm_ar_filepath().clone();

        let mut members = Vec::new();
        for index in 0..objects {
            let source = dir.join(format!("member{index}.c"));
            fs::write(
                &source,
                format!("int member{index}(void) {{ return {index}; }}\n"),
            )
            .expect("failed to write a fixture source");
            let object = dir.join(format!("member{index}.o"));
            let status = Command::new(&clang)
                .arg("-c")
                .arg(&source)
                .arg("-o")
                .arg(&object)
                .status()
                .expect("failed to run clang");
            assert!(status.success(), "compiling the fixture member failed");
            members.push(object);
        }

        let metadata = dir.join("lib.rmeta");
        fs::write(&metadata, b"not an object file\n")
            .expect("failed to write the fixture metadata");
        members.push(metadata);

        let archive = dir.join("libfixture.rlib");
        let status = Command::new(&llvm_ar)
            .arg("r")
            .arg(&archive)
            .args(&members)
            .status()
            .expect("failed to run llvm-ar");
        assert!(status.success(), "packing the fixture archive failed");

        archive
    }

    #[test]
    fn marker_object_carries_the_bitcode_path() {
        let tmp = tempfile::tempdir().unwrap();
        let bitcode = placeholder_bitcode(tmp.path());

        let marker = build_marker_object(&bitcode, tmp.path()).expect("marker built");
        let paths =
            extract_bitcode_filepaths_from_object_file(&marker).expect("marker carries a section");

        // An already-absolute path is recorded verbatim, not canonicalized.
        assert_eq!(paths, vec![bitcode]);
    }

    #[test]
    fn patching_an_archive_reaches_every_object_member() {
        let tmp = tempfile::tempdir().unwrap();
        let archive = build_fixture_archive(tmp.path(), 2);
        let bitcode = placeholder_bitcode(tmp.path());

        let patched = patch_archive(&archive, &bitcode).expect("archive patched");
        assert_eq!(
            patched, 2,
            "object members must be patched and non-object members skipped"
        );

        // There is no archive-level extract helper: `rllvm-get-bc` parses the
        // archive and feeds its members to the parsed-objects helper.
        let data = fs::read(&archive).unwrap();
        let parsed = object::read::archive::ArchiveFile::parse(&*data).expect("archive parses");
        let members: Vec<object::File> = parsed
            .members()
            .filter_map(Result::ok)
            .filter_map(|member| member.data(&*data).ok())
            .filter_map(|member| object::File::parse(member).ok())
            .collect();
        let paths = extract_bitcode_filepaths_from_parsed_objects(&members)
            .expect("members carry sections");

        // Both members carry the same crate-level path, deduplicated on read.
        assert_eq!(paths, vec![bitcode]);
    }
}