1 //! Generate a new Cargo project from a given template
2 //!
3 //! Right now, only git repositories can be used as templates. Just execute
4 //!
5 //! ```sh
6 //! $ cargo generate --git https://github.com/user/template.git --name foo
7 //! ```
8 //!
9 //! or
10 //!
11 //! ```sh
12 //! $ cargo gen --git https://github.com/user/template.git --name foo
13 //! ```
14 //!
15 //! and a new Cargo project called foo will be generated.
16 //!
17 //! TEMPLATES:
18 //!
19 //! In templates, the following placeholders can be used:
20 //!
21 //! - `project-name`: Name of the project, in dash-case
22 //!
23 //! - `crate_name`: Name of the project, but in a case valid for a Rust
24 //! identifier, i.e., `snake_case`
25 //!
26 //! - `authors`: Author names, taken from usual environment variables (i.e.
27 //! those which are also used by Cargo and git)
28 //!
29 //! The template author can define their own placeholders in their
30 //! `cargo-generate.toml` file. This looks like the following:
31 //!
32 //! ```toml
33 //!
34 //! [placeholders]
35 //!
36 //! my-placeholder = { type = "string", prompt = "Hello?", choices = ["hello", "world"], default = "hello", regex = "*" }
37 //!
38 //! use-serde = { type = "bool", prompt = "Add serde support?", default = false }
39 //!
40 //! ```
41 //!
42 //! The user of the template will then be asked the the question in "prompt", and must accept the
43 //! default value (if provided) or enter a custom value which will be checked against "choices" (if
44 //! provided) and regex (if provided).
45 //!
46 //! The placeholder map supports the following keys:
47 //!
48 //! `type` (required): Must be "string" or "bool"
49 //!
50 //! `prompt` (required): A string containing the question to be asked to the user
51 //!
52 //! `default` (optional): The default value to be used if the user just presses enter. Must be
53 //! consistent with `type`
54 //!
55 //! `choices` (optional; string only): Possible values the user may enter
56 //!
57 //! `regex` (optional; string only): Regex to validate the entered string
58 //!
59 //! For automation purposes the user of the template may provide provide a file containing the
60 //! values for the keys in the template by using the `--template-values-file` flag.
61 //!
62 //! The file should be a toml file containing the following (for the example template provided above):
63 //!
64 //! ```toml
65 //!
66 //! [values]
67 //!
68 //! my-placeholder = "world"
69 //!
70 //! use-serde = true
71 //!
72 //! ```
73 //!
74 //! If a key is missing in this file, the user will be requested to provide the entry manually. If
75 //! a key in this file is not part of the original template it will be ignored.
76 //!
77 //! To ensure that no interaction will be requested to the user use the `--silent` flag. Then, if a
78 //! template key is missing an error will be returned and the project generation will fail.
79 //!
80 //! Notice: `project-name` and `crate_name` can't be overriden through this file and must be
81 //! provided through the `--name` flag.
82 //!
83 //! `os-arch` and `authors` also can't be overriden and are derived from the environment.
84
85 #![warn(clippy::unneeded_field_pattern, clippy::match_bool, clippy::get_unwrap)]
86
87 mod app_config;
88 mod args;
89 mod config;
90 mod emoji;
91 mod favorites;
92 mod filenames;
93 mod git;
94 mod ignore_me;
95 mod include_exclude;
96 mod interactive;
97 mod log;
98 mod progressbar;
99 mod project_variables;
100 mod template;
101 mod template_variables;
102
103 pub use args::*;
104
105 use anyhow::{anyhow, bail, Context, Result};
106 use config::{Config, CONFIG_FILE_NAME};
107 use console::style;
108 use favorites::{list_favorites, resolve_favorite_args_and_default_values};
109 use std::{
110 borrow::Borrow,
111 collections::HashMap,
112 env, fs,
113 path::{Path, PathBuf},
114 };
115
116 use tempfile::TempDir;
117
118 use crate::{
119 app_config::{app_config_path, AppConfig},
120 template_variables::{CrateType, ProjectName},
121 };
122 use crate::{git::GitConfig, template_variables::resolve_template_values};
123
generate(mut args: Args) -> Result<()>124 pub fn generate(mut args: Args) -> Result<()> {
125 let app_config = AppConfig::from_path(&app_config_path(&args.config)?)?;
126
127 if args.list_favorites {
128 return list_favorites(&app_config, &args);
129 }
130
131 let default_values = resolve_favorite_args_and_default_values(&app_config, &mut args)?;
132
133 let project_name = resolve_project_name(&args)?;
134 let project_dir = resolve_project_dir(&project_name, &args)?;
135
136 let (template_base_dir, template_folder, branch) = prepare_local_template(&args)?;
137
138 let template_config = Config::from_path(
139 &locate_template_file(CONFIG_FILE_NAME, &template_base_dir, &args.subfolder).ok(),
140 )?;
141
142 check_cargo_generate_version(&template_config)?;
143 let template_values = resolve_template_values(default_values, &args)?;
144
145 println!(
146 "{} {} {}",
147 emoji::WRENCH,
148 style("Generating template").bold(),
149 style("...").bold()
150 );
151
152 expand_template(
153 &project_name,
154 &template_folder,
155 &template_values,
156 template_config,
157 &args,
158 )?;
159
160 println!(
161 "{} {} `{}`{}",
162 emoji::WRENCH,
163 style("Moving generated files into:").bold(),
164 style(project_dir.display()).bold().yellow(),
165 style("...").bold()
166 );
167 copy_dir_all(&template_folder, &project_dir)?;
168
169 if !args.init {
170 args.vcs.initialize(&project_dir, branch)?;
171 }
172
173 println!(
174 "{} {} {} {}",
175 emoji::SPARKLE,
176 style("Done!").bold().green(),
177 style("New project created").bold(),
178 style(&project_dir.display()).underlined()
179 );
180 Ok(())
181 }
182
prepare_local_template(args: &Args) -> Result<(TempDir, PathBuf, String), anyhow::Error>183 fn prepare_local_template(args: &Args) -> Result<(TempDir, PathBuf, String), anyhow::Error> {
184 let (template_base_dir, template_folder, branch) = match (&args.git, &args.path) {
185 (Some(_), None) => {
186 let (template_base_dir, branch) = clone_git_template_into_temp(args)?;
187 let template_folder = resolve_template_dir(&template_base_dir, args)?;
188 (template_base_dir, template_folder, branch)
189 }
190 (None, Some(_)) => {
191 let template_base_dir = copy_path_template_into_temp(args)?;
192 let branch = args.branch.clone().unwrap_or_else(|| String::from("main"));
193 let template_folder = template_base_dir.path().into();
194 (template_base_dir, template_folder, branch)
195 }
196 _ => bail!(
197 "{} {} {} {} {}",
198 emoji::ERROR,
199 style("Please specify either").bold(),
200 style("--git <repo>").bold().yellow(),
201 style("or").bold(),
202 style("--path <path>").bold().yellow(),
203 ),
204 };
205 Ok((template_base_dir, template_folder, branch))
206 }
207
check_cargo_generate_version(template_config: &Option<Config>) -> Result<(), anyhow::Error>208 fn check_cargo_generate_version(template_config: &Option<Config>) -> Result<(), anyhow::Error> {
209 if let Some(Config {
210 template:
211 Some(config::TemplateConfig {
212 cargo_generate_version: Some(requirement),
213 ..
214 }),
215 ..
216 }) = template_config
217 {
218 let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
219 if !requirement.matches(&version) {
220 bail!(
221 "{} {} {} {} {}",
222 emoji::ERROR,
223 style("Required cargo-generate version not met. Required:")
224 .bold()
225 .red(),
226 style(requirement).bold().yellow(),
227 style(" was:").bold().red(),
228 style(version).bold().yellow(),
229 );
230 }
231 }
232 Ok(())
233 }
234
resolve_project_name(args: &Args) -> Result<ProjectName>235 fn resolve_project_name(args: &Args) -> Result<ProjectName> {
236 match args.name {
237 Some(ref n) => Ok(ProjectName::new(n)),
238 None if !args.silent => Ok(ProjectName::new(interactive::name()?)),
239 None => Err(anyhow!(
240 "{} {} {}",
241 emoji::ERROR,
242 style("Project Name Error:").bold().red(),
243 style("Option `--silent` provided, but project name was not set. Please use `--name`.")
244 .bold()
245 .red(),
246 )),
247 }
248 }
249
resolve_template_dir(template_base_dir: &TempDir, args: &Args) -> Result<PathBuf>250 fn resolve_template_dir(template_base_dir: &TempDir, args: &Args) -> Result<PathBuf> {
251 match &args.subfolder {
252 Some(subfolder) => {
253 let template_base_dir = fs::canonicalize(template_base_dir.path())?;
254 let template_dir = fs::canonicalize(template_base_dir.join(subfolder))?;
255
256 if !template_dir.starts_with(&template_base_dir) {
257 return Err(anyhow!(
258 "{} {} {}",
259 emoji::ERROR,
260 style("Subfolder Error:").bold().red(),
261 style("Invalid subfolder. Must be part of the template folder structure.")
262 .bold()
263 .red(),
264 ));
265 }
266 if !template_dir.is_dir() {
267 return Err(anyhow!(
268 "{} {} {}",
269 emoji::ERROR,
270 style("Subfolder Error:").bold().red(),
271 style("The specified subfolder must be a valid folder.")
272 .bold()
273 .red(),
274 ));
275 }
276
277 println!(
278 "{} {} `{}`{}",
279 emoji::WRENCH,
280 style("Using template subfolder").bold(),
281 style(subfolder).bold().yellow(),
282 style("...").bold()
283 );
284 Ok(template_dir)
285 }
286 None => Ok(template_base_dir.path().to_owned()),
287 }
288 }
289
copy_path_template_into_temp(args: &Args) -> Result<TempDir>290 fn copy_path_template_into_temp(args: &Args) -> Result<TempDir> {
291 let path_clone_dir = tempfile::tempdir()?;
292 copy_dir_all(
293 args.path
294 .as_ref()
295 .with_context(|| "Missing option git, path or a favorite")?,
296 path_clone_dir.path(),
297 )?;
298 Ok(path_clone_dir)
299 }
300
clone_git_template_into_temp(args: &Args) -> Result<(TempDir, String)>301 fn clone_git_template_into_temp(args: &Args) -> Result<(TempDir, String)> {
302 let git_clone_dir = tempfile::tempdir()?;
303
304 let remote = args
305 .git
306 .clone()
307 .with_context(|| "Missing option git, path or a favorite")?;
308
309 let git_config = GitConfig::new_abbr(
310 remote.into(),
311 args.branch.to_owned(),
312 args.ssh_identity.clone(),
313 )?;
314
315 let branch = git::create(git_clone_dir.path(), git_config).map_err(|e| {
316 anyhow!(
317 "{} {} {}",
318 emoji::ERROR,
319 style("Git Error:").bold().red(),
320 style(e).bold().red(),
321 )
322 })?;
323
324 Ok((git_clone_dir, branch))
325 }
326
copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()>327 pub(crate) fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
328 fn check_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
329 if !dst.as_ref().exists() {
330 return Ok(());
331 }
332
333 for src_entry in fs::read_dir(src)? {
334 let src_entry = src_entry?;
335 let dst_path = dst.as_ref().join(src_entry.file_name());
336 let entry_type = src_entry.file_type()?;
337
338 if entry_type.is_dir() {
339 check_dir_all(src_entry.path(), dst_path)?;
340 } else if entry_type.is_file() {
341 if dst_path.exists() {
342 bail!(
343 "{} {} {}",
344 crate::emoji::WARN,
345 style("File already exists:").bold().red(),
346 style(dst_path.display()).bold().red(),
347 )
348 }
349 } else {
350 bail!(
351 "{} {}",
352 crate::emoji::WARN,
353 style("Symbolic links not supported").bold().red(),
354 )
355 }
356 }
357 Ok(())
358 }
359 fn copy_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
360 fs::create_dir_all(&dst)?;
361 for src_entry in fs::read_dir(src)? {
362 let src_entry = src_entry?;
363 let dst_path = dst.as_ref().join(src_entry.file_name());
364 let entry_type = src_entry.file_type()?;
365 if entry_type.is_dir() {
366 copy_dir_all(src_entry.path(), dst_path)?;
367 } else if entry_type.is_file() {
368 fs::copy(src_entry.path(), dst_path)?;
369 }
370 }
371 Ok(())
372 }
373
374 check_dir_all(&src, &dst)?;
375 copy_all(src, dst)
376 }
377
locate_template_file<T>( name: &str, template_folder: T, subfolder: &Option<String>, ) -> Result<PathBuf> where T: AsRef<Path>,378 fn locate_template_file<T>(
379 name: &str,
380 template_folder: T,
381 subfolder: &Option<String>,
382 ) -> Result<PathBuf>
383 where
384 T: AsRef<Path>,
385 {
386 let template_folder = template_folder.as_ref().to_path_buf();
387 let mut search_folder = subfolder
388 .as_ref()
389 .map_or_else(|| template_folder.to_owned(), |s| template_folder.join(s));
390 loop {
391 let file_path = search_folder.join(name.borrow());
392 if file_path.exists() {
393 return Ok(file_path);
394 }
395 if search_folder == template_folder {
396 bail!("File not found within template");
397 }
398 search_folder = search_folder
399 .parent()
400 .ok_or_else(|| anyhow!("Reached root folder"))?
401 .to_path_buf();
402 }
403 }
404
resolve_project_dir(name: &ProjectName, args: &Args) -> Result<PathBuf>405 fn resolve_project_dir(name: &ProjectName, args: &Args) -> Result<PathBuf> {
406 if args.init {
407 let cwd = env::current_dir()?;
408 return Ok(cwd);
409 }
410
411 let dir_name = if args.force {
412 name.raw()
413 } else {
414 rename_warning(name);
415 name.kebab_case()
416 };
417 let project_dir = env::current_dir()
418 .unwrap_or_else(|_e| ".".into())
419 .join(&dir_name);
420
421 if project_dir.exists() {
422 Err(anyhow!(
423 "{} {}",
424 emoji::ERROR,
425 style("Target directory already exists, aborting!")
426 .bold()
427 .red()
428 ))
429 } else {
430 Ok(project_dir)
431 }
432 }
433
expand_template( name: &ProjectName, dir: &Path, template_values: &HashMap<String, toml::Value>, template_config: Option<Config>, args: &Args, ) -> Result<()>434 fn expand_template(
435 name: &ProjectName,
436 dir: &Path,
437 template_values: &HashMap<String, toml::Value>,
438 template_config: Option<Config>,
439 args: &Args,
440 ) -> Result<()> {
441 let crate_type: CrateType = args.into();
442 let template = template::substitute(name, &crate_type, template_values, args.force)?;
443 let template = match template_config.as_ref() {
444 None => Ok(template),
445 Some(config) => {
446 project_variables::fill_project_variables(template, config, args.silent, |slot| {
447 interactive::variable(slot)
448 })
449 }
450 }?;
451 let mut pbar = progressbar::new();
452
453 ignore_me::remove_unneeded_files(dir, args.verbose);
454
455 template::walk_dir(
456 dir,
457 template,
458 template_config.and_then(|c| c.template),
459 &mut pbar,
460 )?;
461
462 pbar.join().unwrap();
463
464 Ok(())
465 }
466
rename_warning(name: &ProjectName)467 fn rename_warning(name: &ProjectName) {
468 if !name.is_crate_name() {
469 warn!(
470 "{} `{}` {} `{}`{}",
471 style("Renaming project called").bold(),
472 style(&name.user_input).bold().yellow(),
473 style("to").bold(),
474 style(&name.kebab_case()).bold().green(),
475 style("...").bold()
476 );
477 }
478 }
479