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