[Pre-RFC] Cargo Templates

I’ve been thinking about this lately, and I have a proposal that I’d like some feedback on. Essentially, we can think of creating a project from a template as a special case of code generation, right? The rust community already has a way to do code generation at build time in the form of build.rs files, so why don’t we adapt that style to the template issue?

Essentially, I would like (and at least for the first thing, am working on a prototype) two things:

  1. a rust library with an API that lets me programmatically specify a template. A “template” would essentially just be a bin crate that is executed. I have an example at the bottom of this post, but essentially it will let you build up a series of operations using rust code, and when those operations have been specified, the library would know how to translate those operations to the actual file system contents that make up a template. String interpolation could be built in, but it could also be up to the template creator to add some kind of text templating library (handlebars, askama, etc) if they need it. Or the template library could provide a default text templating feature, but allow it to be relpaced.
  2. The other thing would be a runner for these “template crates.” Much like cargo runs the executable that is produced when a build.rs file is compiled, the executables produced by compiling the “template crates” could have a single “runner” that gives the user of cargo a consistent way to run the “template crate” executables. This is especially important for any input that a template crate might need in order to produce a functional project. The runner would know how to get the author name, crate name, etc, that a template crate could access to generate, for example, a Cargo.toml. (this runner could very simply be cargo, but at least for this post I’m leaving it open to discussion)

Here is an example. This code block is an example of the main.rs of a “template crate.” This would be compiled into an executable that the runner would run in order to produce a project.

extern crate template;
extern crate serde;
#[macro_use]
extern crate serde_derive;

use template::prelude::*;

#[derive(Deserialize)]
struct TemplateArgs {
    some_param: String,
}

fn main() {
    // The library could have standard ways to get input. The runner might take 
    // some inputs and translate them to environment variables, much like how
    // build.rs files do input & output
    let args = Template::args_from_env::<TemplateArgs>()
                        .expect("need to include TEMPLATE_some_param");

    // A vector of "file operations," that describe the transformation from
    // template->project. This shows essentially 3 different "operations."
    let files = vec![
        // first, the library knows how to generate a Cargo.toml, so the template
        // author doesn't have to include a templated Cargo.toml in their project
        // unless they really need more flexibility than the API provides
        CargoToml::builder()
                  .name_from_env() // might expect an environment variable 
                                   // "TEMPLATE_CRATE_NAME" or something, that
                                   // would be set by the runner
                  .author_from_env() // might expect an environment variable
                                     // "TEMPLATE_AUTHOR_NAME" or something, that
                                     // would be set by the runner
                  .license(License::Mit_Apache2) // API would also have a
                                                 // `license_from_env()` method
                  // API would also have similar `.dev_dependencies()` and
                  // `.build_dependencies()` methods
                  .dependencies(vec![("tokio", "0.1")])
                  .build(),

        // next, this would take a string, which happens to come from a file in
        // the template crate but could come from anywhere, fills in the templated
        // parameters using some sort of built-in text templating mechanism, and
        // copies it to a location in the output directory
        File::from_str(include_str!("src/templated/main.rs"))
             .to("src/main.rs")
             .args(&args),

        // This shows a plain file copy, taking some bytes from one file in the
        // template and copying them to a location in the generated project. No
        // interpolation is done
        Blob::from_bytes(include_bytes!("src/some-binary-blob")).to("src/assets/some-binary-blob"),
    ];

    // lastly, we call `create` to take the list of operations and translate them to
    // an actual, on-disk project. it would probably create the project in the $PWD
    // by default, though here I show it specifying an output directory
    if let Err(e) = Template::create(&files).at("./result") {
        eprintln!("{:?}", e);
        ::std::process::exit(1);
    }
}

For the runner, you would need to be able to: specify some way to identify the template crate, pass required input parameters to the template crate, and specify an output location. Since templates would just be crates, retrieving them from crates.io is simple, though I agree with @ssokolow that there should be a simple and unambiguous way to specify that the input is coming from a location on the filesystem. Input to the runner could be through environment variables, or maybe key-value command-line flags that come after a -- like is done for the test runner. I would expect specifying the output directory to work just like cargo new.

I’d be glad to hear any feedback, though I’m more interested about what people think of the concept in general than any specifics about the API, as I’m still very early in the process of developing a prototype for this and am still working out exactly what kinds of APIs are useful.

Thanks for your feedback!

1 Like