generate_from_path/
lib.rs

1//! This crate is a trimmed down version of the `cargo-generate` library,
2//! stripped of all the unnecessary code and dependencies for the purpose
3//! of Pavex project generation.
4//!
5//! Copyright (c) 2018 Ashley Williams
6//!
7//! Permission is hereby granted, free of charge, to any
8//! person obtaining a copy of this software and associated
9//! documentation files (the "Software"), to deal in the
10//! Software without restriction, including without
11//! limitation the rights to use, copy, modify, merge,
12//! publish, distribute, sublicense, and/or sell copies of
13//! the Software, and to permit persons to whom the Software
14//! is furnished to do so, subject to the following
15//! conditions:
16//!
17//! The above copyright notice and this permission notice
18//! shall be included in all copies or substantial portions
19//! of the Software.
20//!
21//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
22//! ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
23//! TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
24//! PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
25//! SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
26//! CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27//! OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
28//! IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
29//! DEALINGS IN THE SOFTWARE.
30use crate::template::create_liquid_object;
31use crate::template_variables::{
32    CrateName, ProjectDir, ProjectName, ProjectNameInput, set_project_name_variables,
33};
34use anyhow::bail;
35use liquid::ParserBuilder;
36use std::collections::HashMap;
37use std::path::{Path, PathBuf};
38use tempfile::TempDir;
39use tracing::info;
40
41mod filenames;
42mod ignore_me;
43mod progressbar;
44mod template;
45mod template_variables;
46
47#[derive(Debug)]
48pub struct GenerateArgs {
49    pub name: String,
50    pub template_dir: PathBuf,
51    pub destination: PathBuf,
52    pub define: HashMap<String, liquid_core::Value>,
53    pub ignore: Option<Vec<String>>,
54    pub overwrite: bool,
55    pub verbose: bool,
56}
57
58pub fn generate(args: GenerateArgs) -> Result<PathBuf, anyhow::Error> {
59    let template_dir = get_source_template_into_temp(&args.template_dir)?;
60    let project_dir = expand_template(template_dir.path(), &args)?;
61
62    copy_expanded_template(template_dir.path(), &project_dir, &args)
63}
64
65fn expand_template(template_dir: &Path, args: &GenerateArgs) -> anyhow::Result<PathBuf> {
66    let mut liquid_object = create_liquid_object(args)?;
67
68    let project_name_input = ProjectNameInput::from(&liquid_object);
69    let destination = ProjectDir::try_from(args)?;
70    let project_name = ProjectName::from(&project_name_input);
71    let crate_name = CrateName::from(&project_name_input);
72    set_project_name_variables(&mut liquid_object, &destination, &project_name, &crate_name)?;
73
74    info!("Destination: {destination}");
75    info!("project-name: {project_name}");
76    info!("Generating template");
77
78    for (key, value) in &args.define {
79        liquid_object.insert(key.into(), value.to_owned());
80    }
81
82    ignore_me::remove_unneeded_files(template_dir, &args.ignore, args.verbose)?;
83    let mut pbar = progressbar::new();
84    let liquid_engine = ParserBuilder::with_stdlib().build()?;
85
86    template::walk_dir(
87        template_dir,
88        &mut liquid_object,
89        &liquid_engine,
90        &mut pbar,
91        args.verbose,
92    )?;
93    Ok(destination.as_ref().to_owned())
94}
95
96fn copy_expanded_template(
97    template_dir: &Path,
98    project_dir: &Path,
99    args: &GenerateArgs,
100) -> anyhow::Result<PathBuf> {
101    info!("Moving generated files into: {}", project_dir.display());
102    copy_dir_all(template_dir, project_dir, args.overwrite)?;
103    info!("Initializing a fresh Git repository");
104    git_init(project_dir)?;
105    info!("Done! New project created in {}", project_dir.display());
106    Ok(project_dir.to_owned())
107}
108
109/// Use the `git` command line tool to initialize a new repository
110/// at the given `project_dir`.
111fn git_init(project_dir: &Path) -> anyhow::Result<()> {
112    let output = std::process::Command::new("git")
113        .arg("init")
114        .arg("-b")
115        .arg("main")
116        .arg(project_dir)
117        .output()?;
118    if !output.status.success() {
119        bail!(
120            "Failed to initialize git repository at {}: {}",
121            project_dir.display(),
122            String::from_utf8_lossy(&output.stderr)
123        );
124    }
125    Ok(())
126}
127
128fn get_source_template_into_temp(template_dir: &Path) -> anyhow::Result<TempDir> {
129    let temp_dir = tempfile::Builder::new().prefix("pavex-new").tempdir()?;
130    copy_dir_all(template_dir, temp_dir.path(), false)?;
131    Ok(temp_dir)
132}
133
134pub(crate) fn copy_dir_all(
135    src: impl AsRef<Path>,
136    dst: impl AsRef<Path>,
137    overwrite: bool,
138) -> anyhow::Result<()> {
139    fn check_dir_all(
140        src: impl AsRef<Path>,
141        dst: impl AsRef<Path>,
142        overwrite: bool,
143    ) -> anyhow::Result<()> {
144        if !dst.as_ref().exists() {
145            return Ok(());
146        }
147
148        for src_entry in fs_err::read_dir(src.as_ref())? {
149            let src_entry = src_entry?;
150            let filename = src_entry.file_name().to_string_lossy().to_string();
151            let entry_type = src_entry.file_type()?;
152
153            if entry_type.is_dir() {
154                if filename == ".git" {
155                    continue;
156                }
157                let dst_path = dst.as_ref().join(filename);
158                check_dir_all(src_entry.path(), dst_path, overwrite)?;
159            } else if entry_type.is_file() {
160                let filename = filename.strip_suffix(".liquid").unwrap_or(&filename);
161                let dst_path = dst.as_ref().join(filename);
162                match (dst_path.exists(), overwrite) {
163                    (true, false) => {
164                        bail!("File already exists: {}", dst_path.display())
165                    }
166                    (true, true) => {
167                        tracing::warn!("Overwriting file: {}", dst_path.display());
168                    }
169                    _ => {}
170                };
171            } else {
172                bail!("Symbolic links not supported")
173            }
174        }
175        Ok(())
176    }
177    fn copy_all(
178        src: impl AsRef<Path>,
179        dst: impl AsRef<Path>,
180        overwrite: bool,
181    ) -> anyhow::Result<()> {
182        fs_err::create_dir_all(&dst)?;
183        for src_entry in fs_err::read_dir(src.as_ref())? {
184            let src_entry = src_entry?;
185            let filename = src_entry.file_name().to_string_lossy().to_string();
186            let entry_type = src_entry.file_type()?;
187            if entry_type.is_dir() {
188                let dst_path = dst.as_ref().join(filename);
189                if ".git" == src_entry.file_name() {
190                    continue;
191                }
192                copy_dir_all(src_entry.path(), dst_path, overwrite)?;
193            } else if entry_type.is_file() {
194                let filename = filename.strip_suffix(".liquid").unwrap_or(&filename);
195                let dst_path = dst.as_ref().join(filename);
196                if dst_path.exists() && overwrite {
197                    fs_err::remove_file(&dst_path)?;
198                }
199                fs_err::copy(src_entry.path(), dst_path)?;
200            }
201        }
202        Ok(())
203    }
204
205    check_dir_all(&src, &dst, overwrite)?;
206    copy_all(src, dst, overwrite)
207}