forked from rojo-rbx/rojo
280 lines
8.6 KiB
Rust
280 lines
8.6 KiB
Rust
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<Self, Self::Err> {
|
|
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(())
|
|
}
|