cargo-elfpak 0.2.0

Cargo subcommand for packaging a binary and its Linux runtime closure
Documentation
use anyhow::{Context, Result, bail};
use cargo_metadata::{Metadata, Package, PackageId, Target};
use std::path::{Path, PathBuf};

pub(crate) fn load(
    current_dir: &Path,
    manifest_path: Option<&Path>,
    locked: bool,
    offline: bool,
    frozen: bool,
) -> Result<Metadata> {
    let mut command = cargo_metadata::MetadataCommand::new();
    command.current_dir(current_dir).no_deps();
    if let Some(path) = manifest_path {
        command.manifest_path(path);
    }
    if let Some(cargo) = std::env::var_os("CARGO") {
        command.cargo_path(PathBuf::from(cargo));
    }
    let mut options = Vec::<String>::new();
    append_flag(&mut options, "--locked", locked);
    append_flag(&mut options, "--offline", offline);
    append_flag(&mut options, "--frozen", frozen);
    command.other_options(options);
    command.exec().context("could not read Cargo metadata")
}

fn append_flag(options: &mut Vec<String>, flag: &str, enabled: bool) {
    if enabled {
        options.push(flag.to_owned());
    }
}

#[derive(Debug)]
pub(crate) struct SelectionContext {
    pub(crate) package: Option<String>,
    pub(crate) binary: Option<String>,
    pub(crate) manifest_path: Option<PathBuf>,
    pub(crate) current_dir: PathBuf,
}

#[derive(Clone, Debug)]
pub(crate) struct Selection {
    pub(crate) package_id: PackageId,
    pub(crate) package_name: String,
    pub(crate) binary_name: String,
}

pub(crate) fn select(metadata: &Metadata, context: &SelectionContext) -> Result<Selection> {
    let package = select_package(metadata, context)?;
    let binary = select_binary(package, context.binary.as_deref())?;

    Ok(Selection {
        package_id: package.id.clone(),
        package_name: package.name.to_string(),
        binary_name: binary.name.clone(),
    })
}

fn select_package<'a>(metadata: &'a Metadata, context: &SelectionContext) -> Result<&'a Package> {
    let workspace_packages = metadata.workspace_packages();
    if let Some(requested) = context.package.as_deref() {
        let matches: Vec<_> = workspace_packages
            .iter()
            .copied()
            .filter(|package| package.name == requested)
            .collect();
        return match matches.as_slice() {
            [package] => Ok(package),
            [] => bail!(
                "package `{requested}` is not a workspace member; available packages: {}",
                package_names(&workspace_packages)
            ),
            _ => bail!(
                "package `{requested}` is ambiguous; select one package with a more specific --package value"
            ),
        };
    }

    if let Some(manifest_path) = context.manifest_path.as_deref() {
        let manifest_path = absolute_path(manifest_path, &context.current_dir);
        if let Some(package) = workspace_packages
            .iter()
            .copied()
            .find(|package| package.manifest_path.as_std_path() == manifest_path)
        {
            return Ok(package);
        }
    }

    if let Some(package) = package_containing(&workspace_packages, &context.current_dir) {
        return Ok(package);
    }
    if let Some(package) = metadata.root_package() {
        return Ok(package);
    }

    if metadata.workspace_default_members.is_available()
        && metadata.workspace_default_members.len() == 1
    {
        let id = &metadata.workspace_default_members[0];
        return workspace_packages
            .iter()
            .copied()
            .find(|package| &package.id == id)
            .with_context(|| format!("Cargo metadata names unknown default package `{id}`"));
    }

    let choices = if metadata.workspace_default_members.is_available()
        && !metadata.workspace_default_members.is_empty()
    {
        workspace_packages
            .iter()
            .copied()
            .filter(|package| metadata.workspace_default_members.contains(&package.id))
            .collect()
    } else {
        workspace_packages
    };
    bail!(
        "Cargo package is ambiguous; select one with --package <PACKAGE> (available: {})",
        package_names(&choices)
    )
}

fn package_containing<'a>(packages: &[&'a Package], current_dir: &Path) -> Option<&'a Package> {
    packages
        .iter()
        .copied()
        .filter_map(|package| {
            let directory = package.manifest_path.parent()?;
            current_dir
                .starts_with(directory.as_std_path())
                .then_some((directory.as_str().len(), package))
        })
        .max_by_key(|(length, _)| *length)
        .map(|(_, package)| package)
}

fn select_binary<'a>(package: &'a Package, requested: Option<&str>) -> Result<&'a Target> {
    let mut binaries: Vec<_> = package
        .targets
        .iter()
        .filter(|target| target.is_bin())
        .collect();
    binaries.sort_by(|left, right| left.name.cmp(&right.name));
    if binaries.is_empty() {
        bail!("package `{}` has no binary targets", package.name);
    }

    if let Some(requested) = requested {
        return binaries
            .iter()
            .copied()
            .find(|target| target.name == requested)
            .with_context(|| {
                format!(
                    "binary `{requested}` does not exist in package `{}`; available binaries: {}",
                    package.name,
                    binary_names(&binaries)
                )
            });
    }

    if let Some(default_run) = package.default_run.as_deref() {
        return binaries
            .iter()
            .copied()
            .find(|target| target.name == default_run)
            .with_context(|| {
                format!(
                    "package `{}` declares unknown default-run binary `{default_run}`",
                    package.name
                )
            });
    }
    if let Some(binary) = binaries
        .iter()
        .copied()
        .find(|target| target.name == package.name.as_ref())
    {
        return Ok(binary);
    }
    if let [binary] = binaries.as_slice() {
        return Ok(binary);
    }

    bail!(
        "package `{}` has multiple binaries; select one with --bin <NAME> (available: {})",
        package.name,
        binary_names(&binaries)
    )
}

fn absolute_path(path: &Path, current_dir: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        current_dir.join(path)
    }
}

fn package_names(packages: &[&Package]) -> String {
    let mut names: Vec<_> = packages
        .iter()
        .map(|package| package.name.as_ref())
        .collect();
    names.sort_unstable();
    names.join(", ")
}

fn binary_names(binaries: &[&Target]) -> String {
    binaries
        .iter()
        .map(|target| target.name.as_str())
        .collect::<Vec<_>>()
        .join(", ")
}

#[cfg(test)]
mod tests {
    use super::{SelectionContext, select};
    use cargo_metadata::Metadata;
    use std::path::PathBuf;

    const WORKSPACE: &str = r#"
    {
      "packages": [
        {
          "name": "api",
          "version": "0.1.0",
          "id": "path+file:///workspace/api#0.1.0",
          "dependencies": [],
          "targets": [
            {"name":"serve","kind":["bin"],"src_path":"/workspace/api/src/bin/serve.rs"},
            {"name":"migrate","kind":["bin"],"src_path":"/workspace/api/src/bin/migrate.rs"}
          ],
          "features": {},
          "manifest_path": "/workspace/api/Cargo.toml",
          "default_run": "serve"
        },
        {
          "name": "worker",
          "version": "0.1.0",
          "id": "path+file:///workspace/worker#0.1.0",
          "dependencies": [],
          "targets": [
            {"name":"worker","kind":["bin"],"src_path":"/workspace/worker/src/main.rs"}
          ],
          "features": {},
          "manifest_path": "/workspace/worker/Cargo.toml"
        }
      ],
      "workspace_members": [
        "path+file:///workspace/api#0.1.0",
        "path+file:///workspace/worker#0.1.0"
      ],
      "workspace_default_members": [
        "path+file:///workspace/api#0.1.0",
        "path+file:///workspace/worker#0.1.0"
      ],
      "resolve": null,
      "target_directory": "/workspace/target",
      "build_directory": "/workspace/target",
      "workspace_root": "/workspace",
      "metadata": {},
      "version": 1
    }
    "#;

    fn workspace() -> Metadata {
        serde_json::from_str(WORKSPACE).unwrap()
    }

    fn context(package: Option<&str>, binary: Option<&str>) -> SelectionContext {
        SelectionContext {
            package: package.map(str::to_owned),
            binary: binary.map(str::to_owned),
            manifest_path: None,
            current_dir: PathBuf::from("/workspace"),
        }
    }

    #[test]
    fn explicit_package_selects_its_package_named_binary() {
        let selected = select(&workspace(), &context(Some("worker"), None)).unwrap();
        assert_eq!(selected.package_name, "worker");
        assert_eq!(selected.binary_name, "worker");
    }

    #[test]
    fn default_run_selects_a_binary() {
        let selected = select(&workspace(), &context(Some("api"), None)).unwrap();
        assert_eq!(selected.binary_name, "serve");
    }

    #[test]
    fn explicit_binary_overrides_default_run() {
        let selected = select(&workspace(), &context(Some("api"), Some("migrate"))).unwrap();
        assert_eq!(selected.binary_name, "migrate");
    }

    #[test]
    fn ambiguous_workspace_requires_package_and_lists_choices() {
        let error = select(&workspace(), &context(None, None)).unwrap_err();
        let text = error.to_string();
        assert!(text.contains("--package"), "{text}");
        assert!(text.contains("api, worker"), "{text}");
    }

    #[test]
    fn unknown_package_lists_choices() {
        let error = select(&workspace(), &context(Some("missing"), None)).unwrap_err();
        let text = error.to_string();
        assert!(text.contains("missing"), "{text}");
        assert!(text.contains("api, worker"), "{text}");
    }

    #[test]
    fn unknown_binary_lists_choices() {
        let error = select(&workspace(), &context(Some("api"), Some("missing"))).unwrap_err();
        let text = error.to_string();
        assert!(text.contains("missing"), "{text}");
        assert!(text.contains("migrate, serve"), "{text}");
    }

    #[test]
    fn current_package_is_inferred_from_the_working_directory() {
        let mut selected_context = context(None, None);
        selected_context.current_dir = PathBuf::from("/workspace/worker/src");
        let selected = select(&workspace(), &selected_context).unwrap();
        assert_eq!(selected.package_name, "worker");
    }

    #[test]
    fn a_package_manifest_path_selects_that_package() {
        let mut selected_context = context(None, None);
        selected_context.manifest_path = Some(PathBuf::from("/workspace/api/Cargo.toml"));
        let selected = select(&workspace(), &selected_context).unwrap();
        assert_eq!(selected.package_name, "api");
    }

    #[test]
    fn a_sole_default_member_is_inferred() {
        let metadata: Metadata = serde_json::from_str(
            &WORKSPACE.replace(
                "\"path+file:///workspace/api#0.1.0\",\n        \"path+file:///workspace/worker#0.1.0\"\n      ],\n      \"resolve\"",
                "\"path+file:///workspace/worker#0.1.0\"\n      ],\n      \"resolve\"",
            ),
        )
        .unwrap();
        let selected = select(&metadata, &context(None, None)).unwrap();
        assert_eq!(selected.package_name, "worker");
    }

    #[test]
    fn multiple_uninferred_binaries_require_bin() {
        let mut metadata = workspace();
        metadata.packages[0].default_run = None;
        metadata.packages[0].name = cargo_metadata::PackageName::new("frontend".to_owned());
        let error = select(&metadata, &context(Some("frontend"), None)).unwrap_err();
        let text = error.to_string();
        assert!(text.contains("--bin"), "{text}");
        assert!(text.contains("migrate, serve"), "{text}");
    }

    #[test]
    fn a_sole_binary_is_inferred() {
        let mut metadata = workspace();
        metadata.packages[1].name = cargo_metadata::PackageName::new("jobs".to_owned());
        let selected = select(&metadata, &context(Some("jobs"), None)).unwrap();
        assert_eq!(selected.binary_name, "worker");
    }
}