use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::str::FromStr; use anyhow::{bail, format_err}; use clap::Parser; use fs_err as fs; use fs_err::OpenOptions; use super::resolve_path; static MODEL_PROJECT: &str = include_str!("../../assets/default-model-project/default.project.json"); static MODEL_README: &str = include_str!("../../assets/default-model-project/README.md"); static MODEL_INIT: &str = include_str!("../../assets/default-model-project/src-init.luau"); static MODEL_GIT_IGNORE: &str = include_str!("../../assets/default-model-project/gitignore.txt"); static PLACE_PROJECT: &str = include_str!("../../assets/default-place-project/default.project.json"); static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md"); static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt"); static PLUGIN_PROJECT: &str = include_str!("../../assets/default-plugin-project/default.project.json"); static PLUGIN_README: &str = include_str!("../../assets/default-plugin-project/README.md"); static PLUGIN_GIT_IGNORE: &str = include_str!("../../assets/default-plugin-project/gitignore.txt"); /// Initializes a new Rojo project. #[derive(Debug, Parser)] pub struct InitCommand { /// Path to the place to create the project. Defaults to the current directory. #[clap(default_value = "")] pub path: PathBuf, /// The kind of project to create, 'place', 'plugin', or 'model'. Defaults to place. #[clap(long, default_value = "place")] pub kind: InitKind, } impl InitCommand { pub fn run(self) -> anyhow::Result<()> { let base_path = resolve_path(&self.path); fs::create_dir_all(&base_path)?; let canonical = fs::canonicalize(&base_path)?; let project_name = canonical .file_name() .and_then(|name| name.to_str()) .unwrap_or("new-project"); let project_params = ProjectParams { name: project_name.to_owned(), }; match self.kind { InitKind::Place => init_place(&base_path, project_params)?, InitKind::Model => init_model(&base_path, project_params)?, InitKind::Plugin => init_plugin(&base_path, project_params)?, } println!("Created project successfully."); Ok(()) } } /// The templates we support for initializing a Rojo project. #[derive(Debug, Clone, Copy)] pub enum InitKind { /// A place that contains a baseplate. Place, /// An empty model, suitable for a library. Model, /// An empty plugin. Plugin, } impl FromStr for InitKind { type Err = anyhow::Error; fn from_str(source: &str) -> Result { match source { "place" => Ok(InitKind::Place), "model" => Ok(InitKind::Model), "plugin" => Ok(InitKind::Plugin), _ => Err(format_err!( "Invalid init kind '{}'. Valid kinds are: place, model, plugin", source )), } } } fn init_place(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> { println!("Creating new place project '{}'", project_params.name); let project_file = project_params.render_template(PLACE_PROJECT); try_create_project(base_path, &project_file)?; let readme = project_params.render_template(PLACE_README); write_if_not_exists(&base_path.join("README.md"), &readme)?; let src = base_path.join("src"); fs::create_dir_all(&src)?; let src_shared = src.join("shared"); fs::create_dir_all(src.join(&src_shared))?; let src_server = src.join("server"); fs::create_dir_all(src.join(&src_server))?; let src_client = src.join("client"); fs::create_dir_all(src.join(&src_client))?; write_if_not_exists( &src_shared.join("Hello.luau"), "return function()\n\tprint(\"Hello, world!\")\nend", )?; write_if_not_exists( &src_server.join("init.server.luau"), "print(\"Hello world, from server!\")", )?; write_if_not_exists( &src_client.join("init.client.luau"), "print(\"Hello world, from client!\")", )?; let git_ignore = project_params.render_template(PLACE_GIT_IGNORE); try_git_init(base_path, &git_ignore)?; Ok(()) } fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> { println!("Creating new model project '{}'", project_params.name); let project_file = project_params.render_template(MODEL_PROJECT); try_create_project(base_path, &project_file)?; let readme = project_params.render_template(MODEL_README); write_if_not_exists(&base_path.join("README.md"), &readme)?; let src = base_path.join("src"); fs::create_dir_all(&src)?; let init = project_params.render_template(MODEL_INIT); write_if_not_exists(&src.join("init.luau"), &init)?; let git_ignore = project_params.render_template(MODEL_GIT_IGNORE); try_git_init(base_path, &git_ignore)?; Ok(()) } fn init_plugin(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> { println!("Creating new plugin project '{}'", project_params.name); let project_file = project_params.render_template(PLUGIN_PROJECT); try_create_project(base_path, &project_file)?; let readme = project_params.render_template(PLUGIN_README); write_if_not_exists(&base_path.join("README.md"), &readme)?; let src = base_path.join("src"); fs::create_dir_all(&src)?; write_if_not_exists( &src.join("init.server.luau"), "print(\"Hello world, from plugin!\")\n", )?; let git_ignore = project_params.render_template(PLUGIN_GIT_IGNORE); try_git_init(base_path, &git_ignore)?; Ok(()) } /// Contains parameters used in templates to create a project. struct ProjectParams { name: String, } impl ProjectParams { /// Render a template by replacing variables with project parameters. fn render_template(&self, template: &str) -> String { template .replace("{project_name}", &self.name) .replace("{rojo_version}", env!("CARGO_PKG_VERSION")) } } /// Attempt to initialize a Git repository if necessary, and create .gitignore. fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> { if should_git_init(path) { log::debug!("Initializing Git repository..."); let status = Command::new("git").arg("init").current_dir(path).status()?; if !status.success() { bail!("git init failed: status code {:?}", status.code()); } } write_if_not_exists(&path.join(".gitignore"), git_ignore)?; Ok(()) } /// Tells whether we should initialize a Git repository inside the given path. /// /// Will return false if the user doesn't have Git installed or if the path is /// already inside a Git repository. fn should_git_init(path: &Path) -> bool { let result = Command::new("git") .args(["rev-parse", "--is-inside-work-tree"]) .stdout(Stdio::null()) .stderr(Stdio::null()) .current_dir(path) .status(); match result { // If the command ran, but returned a non-zero exit code, we are not in // a Git repo and we should initialize one. Ok(status) => !status.success(), // If the command failed to run, we probably don't have Git installed. Err(_) => false, } } /// Write a file if it does not exist yet, otherwise, leave it alone. fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> { let file_res = OpenOptions::new().write(true).create_new(true).open(path); let mut file = match file_res { Ok(file) => file, Err(err) => { return match err.kind() { io::ErrorKind::AlreadyExists => return Ok(()), _ => Err(err.into()), } } }; file.write_all(contents.as_bytes())?; Ok(()) } /// Try to create a project file and fail if it already exists. fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Error> { let project_path = base_path.join("default.project.json"); let file_res = OpenOptions::new() .write(true) .create_new(true) .open(&project_path); let mut file = match file_res { Ok(file) => file, Err(err) => { return match err.kind() { io::ErrorKind::AlreadyExists => { bail!("Project file already exists: {}", project_path.display()) } _ => Err(err.into()), } } }; file.write_all(contents.as_bytes())?; Ok(()) }