fission-command-core 0.11.1

Shared project, manifest, target, and diagnostic model for the fission command
Documentation
use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::process::Command;

pub(crate) const CARGO_TARGET_DIRECTORY_TOKEN: &str = "{cargo_target_dir}";

#[derive(Deserialize)]
struct CargoMetadata {
    target_directory: PathBuf,
}

pub(crate) fn cargo_target_directory(
    project_dir: &Path,
    manifest: &Path,
    module_name: &str,
    platform: &str,
) -> Result<PathBuf> {
    let output = Command::new("cargo")
        .arg("metadata")
        .arg("--format-version")
        .arg("1")
        .arg("--no-deps")
        .arg("--manifest-path")
        .arg(manifest)
        .current_dir(project_dir)
        .output()
        .with_context(|| {
            format!(
                "failed to resolve the Cargo target directory for {platform} native module `{module_name}`"
            )
        })?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
        bail!(
            "Cargo metadata failed for {platform} native module `{module_name}` with {}{}",
            output.status,
            if stderr.is_empty() {
                String::new()
            } else {
                format!(": {stderr}")
            }
        );
    }
    let metadata: CargoMetadata = serde_json::from_slice(&output.stdout).with_context(|| {
        format!("Cargo returned invalid metadata for {platform} native module `{module_name}`")
    })?;
    if !metadata.target_directory.is_absolute() {
        bail!(
            "Cargo returned a non-absolute target directory for {platform} native module `{module_name}`: {}",
            metadata.target_directory.display()
        );
    }
    Ok(metadata.target_directory)
}

pub(crate) fn expand_cargo_target_directory(
    value: &str,
    target_directory: Option<&Path>,
    module_name: &str,
    platform: &str,
) -> Result<String> {
    if !value.contains(CARGO_TARGET_DIRECTORY_TOKEN) {
        return Ok(value.to_owned());
    }
    let target_directory = target_directory.with_context(|| {
        format!(
            "{platform} native module `{module_name}` uses {CARGO_TARGET_DIRECTORY_TOKEN} for a product that is not built by Cargo"
        )
    })?;
    Ok(value.replace(
        CARGO_TARGET_DIRECTORY_TOKEN,
        &target_directory.to_string_lossy(),
    ))
}

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

    #[test]
    fn expands_the_configured_cargo_target_directory() {
        let expanded = expand_cargo_target_directory(
            "{cargo_target_dir}/release/demo-helper",
            Some(Path::new("/shared/cargo")),
            "demo",
            "Linux",
        )
        .unwrap();

        assert_eq!(expanded, "/shared/cargo/release/demo-helper");
    }

    #[test]
    fn rejects_the_token_for_non_cargo_products() {
        let error = expand_cargo_target_directory(
            "{cargo_target_dir}/release/demo-helper",
            None,
            "demo",
            "Windows",
        )
        .unwrap_err();

        assert!(error.to_string().contains("not built by Cargo"));
    }
}