Alternative to `cargo new` templates: examples as build-target templates

Looking for thoughts on an alternative idea to what we normally think of as templates for cargo new and some brainstorming on the CLI.

From both the Cargo team and the community, there has long been an interest to having cargo generate-like templates in cargo new (Feature request: Cargo templates · Issue #5151 · rust-lang/cargo · GitHub). However, this is not trivial and has needed someone to work through problems like:

  • Working out the template language to pull into our compatibility guarantees
  • Deciding on the API surface between cargo new and the template, either being exhaustive or having a plan for evolving it
  • Should we and how do we preserve the "smart" features like auto-inheriting
  • How to handle updating from "living" templates. All of my repos have a common git merge base that is the template and this makes it easy to propagate changes out, like with lints. I couldn't live without it
  • How to handle composition. There are project templates, and package templates and how to allow intermixing of them

On that last point, in other discussions I've realized that there is another layer of templates: API templates. These are not whole-package templates but build-target or even mod-specific templates. You might have a CLI template that gets you started with CLI parser, a logger, etc.You might then have a CLI test template that does end-to-end testing of your CLI, capturing SVG snapshots of the output to preserve the colors. You then might find you need to do file watching so you want to pull in a notify template as a mod.

My idea has two parts:

  • cargo new becomes additive, allowing cargo new --lib when there is already a package present, adding a lib
  • We allow specifying a package's examples as your src/lib.rs or src/main.rs. Even better if it could also be build.rs and tests/foo.rs. Amazing if we could also do src/foo/mod.rs.

This way you can pull in examples as you need the behavior and then start editing from there to get what you want.

If we limit this to examples you already have as dependencies, then we could look at the examples required-features and automatically add the features and dependencies that that activates. An example can also rely on dev-dependencies but we don't have a good story for detecting which are relevant and likely would have to just let those example-templates be broken. Pulling in a frontmatter might also be a way to customize the dependencies. This can also help users reading the example have a more precise dependency list. The downside is that there wouldn't be an easy way to validate this

Sometimes there are other settings that are relevant, like harness = false. If the person has tests or benches enabled for their example, we could read that and copy it over to their package.

Limiting to existing dependencies makes this a bit easier to operate on. However, if we could find a way to make this also work with any package, then this would be amazing for Cargo Script. A big priority for Cargo Script is that it should be light weight enough not to need cargo new support. I've intentionally avoided it to help draw attention to the pain points involved in hand writing them so we can address them.

However, if cargo new could give you more than

#!/usr/bin/env cargo
---
package.edition = "2024"
---

fn main() {
}

and instead could give you

#!/usr/bin/env cargo
---
package.edition = "2024"

[dependencies]
clap = { version = "4.5.30", features = ["derive"] }
---

use clap::Parser;

#[derive(Parser)]
struct Cli {
}

fn main() {
    let cli = Cli::parse();
}

That would be much more powerful.

However, where I'm stuck on this idea is how to design the CLI, keeping in mind

  • build-targets are additive
  • we'd support more kinds of build targets
  • ideally, also support mods
  • select a dependency
  • select the example within the dependency, helping the user choose
  • maybe even allow this for non-dependencies
  • align with Cargo's CLI principles
  • fit within cargo new

Unresolved questions

  • Could we somehow have descriptions for showing the user, e.g. in an interactive mode?
  • Could maintainers specify a default example?
    • How do we handle this on the CLI? If we use a variable num_args in clap, the positional argument can be mistaken for an example
  • What level of built-in validation should we provide vs being done by third-party tools (kind of like the role cargo hack plays)
4 Likes

One option could be:

$ cargo new --bin --from clap
... list all examples ...
$ cargo new --bin --from clap demo

would create

Cargo.toml:

...

[dependencies]
clap = { version = "4.5.30", features = ["derive" }

src/main.rs:

use clap::Parser;

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    /// Name of the person to greet
    #[arg(short, long)]
    name: String,

    /// Number of times to greet
    #[arg(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
    let args = Args::parse();

    for _ in 0..args.count {
        println!("Hello {}!", args.name);
    }
}

There are several routes we can take for copying the example:

  • If its examples/foo.rs, copy that in. If its a examples/foo/main.rs then copy it to main.rs, lib.rs, or mod.rs along with the rest of the directory
    • Apparently, cargo doesn't discover examples/foo/lib.rs though lib examples are supported by setting crate-types in Cargo.toml
  • Should we limit lib examples to only being used for mods and lib.rs? Or do we auto-append fn main()?
  • Should we limit bin examples to main.rs, tests, and build scripts? Or copy them, over and have main.rs be dead code?

For mods, maybe this would be:

$ cargo new --mod src/foo --from clap demo

If the mod exists, it will be used as the parent for the new mod. If it isn't, then it will be used.

For Cargo script, it would be:

$ cargo new --file --from clap demo

where it defaults the filename to the example name if a path isn't given.

In terms of example discover, cargo new --from <pkg> could list them out like cargo test --test does for tests.

Other ways to help with example discovery are

  • -i / --interactive mode
  • docs.rs could have a tab for listing examples and they could provide an example command for using the example like crates.io does with cargo add and cargo install

Thoughts:

  • Not thrilled with multi-valued flags
  • We could accept a PackageIdSpec for non-dependency packages
    • Full-form is verbose and hard to write out (registry+https://github.com/rust-lang/crates.io-index#clap@4.5.56)
    • If we support short form (clap, clap@4.5.56), we could get some dependency confusion going on (thought you were adding an example from a package in a local dependency that points to git or an alt registry but instead get from crates.io. If we support a default registry in Cargo.toml as suggested at #t-cargo > Making alternative registries nicer to use @ :speech_balloon:, then this might limit the impact but not remove it.
1 Like

Some random bits of intuition as to what commands might be available and do:

  • cargo new without --bin/--lib/etc in an existing workspace would maintain the same behavior.
    • The intuited behavior in this post probably conflicts in multiple ways with intuitive behavior for creating new workspace crates and/or current behavior. The purpose of this exercise was to only consider what I might intuitively expect when working on a single package, although I chose wording that acknowledges the difference between workspace and package.
  • cargo new --bin in an existing workspace would add an implicit main.rs binary target to the closest package, erroring if it already exists.
  • cargo new --lib in an existing workspace would add an implicit lib.rs library target to the closest package, erroring if it already exists.
  • cargo new --mod {path} would create a new module at {path}.rs and add the mod statement to the parent module file.
    • This is the existing {path} argument, and is mandatory.
    • Error if the parent module is not present as a file.
    • If not within {srcdir} and interactive, ask if the user wants to create the module {srcdir}/{path}.rs instead, otherwise error.
  • cargo new --example {path} would create a new example file at {path}.rs.
    • This is the existing {path} argument, and is mandatory.
    • If you want a submodule in the examples directory instead, you must use --example --mod.
    • Error if not an auto-discovered example location, unless --name is provided, in which case add an entry to [[examples]].
  • cargo new --build would add a build.rs to the closest package.
    • No {path} necessary.
    • Don't support --build --mod, that's a bit too niche of a use case.
  • cargo new --script {path} would initialize a file with the appropriate header for cargo-script.
  • cargo new --from {crate} would initialize the created source file with the default template from {crate}.
    • {crate} must be either:
      • a direct dependency of the closest package, using renamed names;
      • a dependency named in the workspace dependencies table, using renamed names; or
      • a package in the lockfile with only a single version present.
    • {crate}@{version} can also be used, in which case that package with that version constraint is used from the default registry.
  • cargo new --from {crate}/{example} would use the specified example instead of the default example template.

Also, all examples are currently required to be binary crates, iirc. Using an example as a library or module template could choose to leave the unused fn main or strip it.


An extra note I don't think you mentioned — dependencies used by the template/example that may not be dependencies of the local package target already. This is an added motivator for [[example-dependencies]] and perhaps even a dependencies table for specific examples.

I have a crate that would greatly enjoy this, although it would require example dependencies and likely also the ability for this to include auxiliary support files. It would be entirely straightforward to include this information manually in the manifest; I don't expect it to be inferred.

One problem with using this is that a {crate} likely should support the fully Package Id Spec format which would is a URL.

Also, any thoughts on how we help with example discovery?

It's not exactly the prettiest solution, but we could require that {proto}://{hostname-and-path} package ids include the # section, e.g.

spec := pkgname [ "/" template-name ] |
        [ kind "+" ] proto "://" hostname-and-path [ "?" query ]
                [ "#" [ pkgname | semver ] [ "/" template-name ] ]

To make that nicer, we could allow the registry+ kind to directly serve packages/templates, such that something like https://crates.internal/clap/full is shorthand for registry+https://crates.internal#clap:*/full.

The rustdoc feature to show package examples in API documentation is a big one. Examples marked as templates should be surfaced in some manner on the cratesio page, although I don't have any real suggestions as to how. (Perhaps just by name and top-level doc comment? In a subpage, like for versions/dependencies/etc. Plus linking to the docs page below, or to the source page until such exists.)

A longshot idea would be for docsrs to, alongside https://docs.rs/clap/latest/clap for the API documentation and https://docs.rs/crate/clap/latest/source/examples/ for the examples' source directory, create a page https://docs.rs/crate/clap/latest/examples/ for browsing examples with nicer navigation than the source view (i.e. closer to what you get if you abuse mod API documentation to present examples). Bonus points if this allows for some form of literate programming, interleaving rendered markdown content and code snippets. Once this exists, then crate resources and documentation can link it if it's useful to downstream consumers.

1 Like

(Just a use case) I would love to be able to do something like this and would use it all the time:

cargo new --bench --from criterion getting-started

And after that, have the whole Getting Started example from Criterion's documentation ready to be run with:

cargo bench
1 Like

Currently, this design does not have a way to know about harness = false.

If the example author marks their example as a bench and sets it, maybe we should copy it over?

Below are some thought's I've loosely grouped together. I really like the idea of official template support in Cargo, but I think there's a lot of possibilities worth exploring!

Examples (probably) Shouldn't Be Templates

I don't think we want to directly pull examples from existing crates as templates. A larger crate, like bevy has many excellent examples, but the dev-dependencies section is massive in order to accommodate all the different kinds of examples it showcases. I feel like you'd want an explicit template item to pull from, which would just be a crate (either a single-file script or a more typical crate). The benefit of the new frontmatter system is you could now have single-file templates, which would just look identical to examples anyway. The key difference is they're explicitly a separate crate, so they have their own dependencies, enabled features, etc.

Since a template section would be new, it could also have explicit support for a default. This would let a crate like clap provide a template that fits most users, and then some extra templates for useful niches. This would also solve having template kinds that go beyond the currently understood set of tests, benchmarks, libraries, etc. For example, a mod template could still include a frontmatter that gets merged with the target crate's manifest, and then it's body would be a new module.

Macros?

If templates are meant to include extending an existing project, not just creating a new one, I feel like we need either a build script or a proc-macro to handle merging a template into a project. Take tokio and clap as examples. They each could have templates for a new binary project, and ideally you could slap tokio onto an existing project and it would handle taking the contents of your existing main function and placing them into the new async version for you. This probably can't be done by Cargo since it's far too contextual, but I think either a merge.rs or a merge proc-macro could do it.

--interactive

I do think a TUI more like npm init would be useful, especially for new users. For one thing, it could give you an option to search for a template from crates.io, select features and dependencies, etc. I imagine you could add a --interactive/-I switch to turn this on (in order to preserve the current fast non-interactive command).

1 Like

I like the idea of splitting templates and making them incremental. Tauri and Bevy have lots of optional functionality. Setting everything upfront requires complex interactive generators, and is too much for a new user anyway. These projects would benefit from adding one plugin at a time.

Trying to use targets of one package with unique per-target dependencies is another case where targets in a package want to be more like packages within a workspace. For discoverability in Cargo you'd also want to have descriptions for them, which the package has, but targets don't.

Perhaps this is where publishing of workspaces to crates.io would help? Templates could be implemented as packages, with clearly defined dependencies (dev too), and their own features.

Maybe even build.rs equivalent for integration into an existing project? Bevy needs to add plugins to initialization stage, register resources and types for reflection, etc. Tauri similarly has an app init boilerplate to update, as well as a bunch of JSON files with per-plugin permissions to set.

2 Likes

Yes, dependencies are an issue. There is required-features but that doesn't solve everything.

Adding a new template system also has a lot of complications to work through:

  • Associating: how does a package declare a relationship to its templates when those templates depend on it? This is one of the problems we are having with the "recommended-bins" idea for libraries to redirect people to their associated binary (e.g. a cargo install foo and tell people to instead run cargo install foo-cli)
  • Being able to tell what to copy over. With an example this is easy. With a whole package, it gets more complicated.
  • MSRV: if this ends up requiring an MSRV bump, it will take even longer before people can use it
  • Bootstrapping: packages have examples today. Maintainers would then need to write out templates and likely dual-source some examples as templates

Nothing stops people from putting frontmatter in their examples today and in fact it might be useful to do so as people get confused reading an example and understanding how to replicate it. Even if we do example templates, we could merge the frontmatter in as well.

Having defaults would be an interesting idea regardless of what is used as the example.

For clap, I don't think there is anything I would do for a merge operation. For CLI testing, I could see customizing the environment variable read for CARGO_BIN_EXE_*. But I've accepted the idea of not doing that.

Build scripts as is are getting a lot of scrutiny for security a and build performance and we should be looking for ways to remove their need or consolidate down to fewer of them. At least with build scripts, you can depend on everything and then audit it before running. Here, you would be running foreign code and the workflow for auditing it is worse and you wouldn't even know the version used.

If we have a merge.rs, we then also need merge-dependencies and the run on effects of that (e.g. cargo new time to compile all of this). We also then need a more native way of validating templates while before I was assuming we could rely on third-party tools which then further extends the API surface in this design work which is one of the things this design was trying to minimize.

There is interest in adding interactive modes to Cargo commands. We are testing the waters with a TUI for cargo tree, see GitHub - orhun/cargo-tree-tui: Ratatuifying Rust's package manager · GitHub

Good point about descriptions.

1 Like

Might be my misunderstanding, but I don't think this is a problem? Since the template wouldn't be a crate published on crates.io, the hypothetical templates section wouldn't need to link to a published crate, so there's no chicken and egg publishing issue. While a template is a crate, it would just be file(s) as far as Cargo and crates.io are concerned, entirely contained within the foo crate.

That's part of the reason I feel like a proc-macro or merge.rs script might be a good starting point for "additive" templates. But for an initial implementation, I don't see why a template couldn't just be a straight copy-paste. cargo new already only makes sense for brand new projects, so there's no merging functionality required.

cargo automatically picks up examples within an examples/ folder, so I feel like picking up templates in a templates/ folder would be fine? That would allow zero configuration within any Cargo.toml, so fully backwards compatible. The only issue I could forsee is if crates already have a templates/ folder that isn't intended for this. In that case, I feel like you could treat invalid templates as a warning, then make it an error at some point in the future.

While templates and examples are related, I don't think they're the same thing. I see examples (currently) coming in two flavours:

  1. How you should use the crate. Such as tokio/hello_world
  2. How you could use the crate. Such as tokio/custom-executor

If there was an actual templates folder, type 1 examples could be moved to templates, while type 2 would remain as an example. Even without official cargo support, I think this would just be a good structural change for projects to adopt; when a user first wants to start using bevy, they'll open the examples folder and see a huge list of examples, which is excellent! But it's hard for a new user to know which examples are type 1 and which are type 2.

This does have a doubling up issue too though, since example dependencies must be declared in dev-dependencies, and within the frontmatter (for the frontmatter to be useful). I see issues when those two lists de-synchronize.

I do think a merge.rs is less of a security concern than build.rs though, since it would be run exactly once when a user calls cargo new. I do agree it's a security concern though, and it'd be a hard sell if there wasn't some sandboxing. That's part of the reason I like the idea of a proc-macro instead of a build script, since it's possible they could be built in such a way to prevent linking to host APIs and just be pure "tokens in -> tokens out" functions.

So your proposal is for there to be crates that exist but aren't part of the package or workspace that get bundled in the package? I feel like that could be confusing.

Please re-read my original post. Making cargo new additive is a major part of this idea.

1 Like

I mean, is it any different to tests, benchmarks, or examples as-is? docs.rs will include snippets of code from examples in documentation, but examples aren't themselves published to crates.io, so there is definitely precedent here.

Sorry I think I worded that badly, what I meant was for an MVP on templates we don't need to consider the additive functionality yet, that can be added after there's a mechanism for templates. Whereas, I don't think you can make cargo new additive without templates (aside from just creating empty files I guess?)

I'm looking at it from the users perspective. They put Cargo,tomls down that are "part of their package" but cargo check --workspace doesn't build them?

I think there is a major gap between our thought processes and/or understandings on this.

In my mind, there are distinct needs between project, package, and API templates and if we want a single solution to cover their needs then we would need to do a serious analysis of all of them to make sure we don't back ourselves into a corner. My suspicion is that there are major divergences in needs. Part of the intent of this proposal is to remove a lot of the complexity that comes with handling project and package templates by focusing in on API templates which have a much smaller feature set and intersections with existing dynamic behavior in cargo new that we would likely want to keep. This feels like it fits well within cargo news existing behavior and will compose with project or package templates just as well as the existing cargo new behavior.

The whole proposal is about having additive templates in cargo new that aren't empty files. To say you we can't do it requires some clearly articulated reasoning to back such a statement.

1 Like