What's the most elegant way to implement Cargo plugins?

Currently, I’ve implemented two cargo plugins, ros_new and ros_add. Under the hood, they’re just advanced cargo drivers that add ros2 related package.xml handling. Ok, I’ll show, what I mean. For Example in ros_add, to manipulate the Cargo.toml, I’m doing the following:

    if !(matches.get_flag("no_cargo_toml") || matches.get_flag("buildtool"))
        && Path::new("Cargo.toml").exists()
    {
        println!("Attempting to add '{package_name}' to Cargo.toml...");
        let mut cargo_add = Command::new("cargo");
        cargo_add.arg("add").arg(dependency);
        // Add --color if specified
        if let Some(color) = matches.get_one::<String>("color") {
            cargo_add.args(["--color", color]);
        }

        // Only add --build if the flag was explicitly given
        if matches.get_flag("build") || matches.get_flag("--build-export") {
            cargo_add.arg("--build");
        }

        PathDoc::new("Cargo.toml".to_string(), dependency_type)
            .map(|ok| ok.add_dependency_to_cargo_toml(package_name))
            .unwrap_or_else(|dings| {
                eprintln!("{dings}");
                Err(dings)
            })
            .unwrap_or_else(|dings| println!("{dings}"));
    }

As you can see, the first step is simply to run Cargo with the dependency name and some Cargo flags. Then I need to run my own Cargo.toml editing mechanic because, regardless of why Cargo fails, it sends error 101, which doesn't tell me anything. Now that I’ve implemented them, I’m wondering if there’s a more elegant way of doing things. I mean, Cargo itself is a Rust crate, right? Is it possible to implement a Cargo plugin by using Cargo as a dependency and extending it, rather than running Cargo under the hood? Has anyone done that before?

It's possible to use Cargo as a library, but you won't get more informative error handling out of it. Cargo uses anyhow::Error for almost everything, so in the end you still have to parse arbitrary text that wasn't meant to be parsed.

Other downsides of using Cargo as a library:

  • much longer compilation time for your tool, since Cargo is a big project with many dependencies
  • if you don't keep the Cargo dependency up to date, your tool will eventually stop working with newer Cargo.toml files, configs, or rustc versions.
  • Cargo makes semver-major changes regularly, so you have to keep updating your integration code.
1 Like

A few more

  • as users update and use new fields, your tool will report "warning: unused manifest field" even if it doesn't matter to your tool. Or worse, the field could have been unstable but now stabilized and your tool fails.
  • cargo-the-library is prioritized around supporting cargo-the-bin and so has limitations, usability problems, poor end-user documentation, without any priority for addressing these. We primarily point people to toml_edit, cargo-util-schemas, or cargo metadata as APIs for plugins to use which won't suite your needs. We do have work exploring a more flexible set of plumbing commands for cargo but you are wanting to extend a porcelain command rather than build something new.
3 Likes
  • What do you mean by unused fields? I've already included some tests for my tools and not experienced such a Warning.
  • Do you mean man by 'porcelain command' something very fragile? Sorry, I'm not a english native and haven't heard that before.
  • Yes, good documentation is always key when it comes to adapting technologies. I myself struggle with this discipline when it comes to providing good documentation for the crates I publish. In fact, it's my difficulties with Cargo documentation that prompted me to start this thread.

What do you mean by unused fields? I've already included some tests for my tools and not experienced such a Warning.

If a Cargo.toml has an unknown field, Cargo will warn instead of ignoring it or erroring. Warning or not is limited by what fields Cargo knows about. If you release today and next release we come up with package.foo, your tool will warn about that field until you update the version of Cargo, release, and your users upgrade.

Do you mean man by 'porcelain command' something very fragile? Sorry, I'm not a english native and haven't heard that before.

Git uses the terms "porcelain" to refer to user facing commands and "plumbing" to refer to low level APIs, see Git - Plumbing and Porcelain. These terms are an analogy to housing. Toilets and other plumbing fixtures (what you interact with) are made from porcelain as a way to interact with the raw plumbing behind them.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.