Modernize the init subcommand

This commit is contained in:
Lucien Greathouse
2021-05-20 17:34:45 -04:00
parent 5c80cd6e50
commit d153f62b8a
4 changed files with 76 additions and 103 deletions

View File

@@ -13,6 +13,7 @@
* Empty `.model.json` files will no longer cause errors. ([#420][pr-420]) * Empty `.model.json` files will no longer cause errors. ([#420][pr-420])
* When specifying `$path` on a service, Rojo now keeps the correct class name. ([#331][issue-331]) * When specifying `$path` on a service, Rojo now keeps the correct class name. ([#331][issue-331])
* Improved error messages for misconfigured projects. * Improved error messages for misconfigured projects.
* Improved error output for many subcommands.
[issue-331]: https://github.com/rojo-rbx/rojo/issues/331 [issue-331]: https://github.com/rojo-rbx/rojo/issues/331
[issue-369]: https://github.com/rojo-rbx/rojo/issues/369 [issue-369]: https://github.com/rojo-rbx/rojo/issues/369

View File

@@ -7,11 +7,11 @@ use librojo::cli::{self, GlobalOptions, Options, Subcommand};
fn run(global: GlobalOptions, subcommand: Subcommand) -> anyhow::Result<()> { fn run(global: GlobalOptions, subcommand: Subcommand) -> anyhow::Result<()> {
match subcommand { match subcommand {
Subcommand::Init(init_options) => cli::init(init_options)?, Subcommand::Init(subcommand) => subcommand.run()?,
Subcommand::Serve(serve_options) => cli::serve(global, serve_options)?, Subcommand::Serve(serve_options) => cli::serve(global, serve_options)?,
Subcommand::Build(build_options) => cli::build(build_options)?, Subcommand::Build(build_options) => cli::build(build_options)?,
Subcommand::Upload(upload_options) => cli::upload(upload_options)?, Subcommand::Upload(upload_options) => cli::upload(upload_options)?,
Subcommand::FmtProject(fmt_options) => fmt_options.run()?, Subcommand::FmtProject(subcommand) => subcommand.run()?,
Subcommand::Doc => cli::doc()?, Subcommand::Doc => cli::doc()?,
Subcommand::Plugin(plugin_options) => cli::plugin(plugin_options)?, Subcommand::Plugin(plugin_options) => cli::plugin(plugin_options)?,
} }

View File

@@ -1,13 +1,14 @@
use std::{ use std::io::{self, Write};
fs::{self, OpenOptions}, use std::path::{Path, PathBuf};
io::{self, Write}, use std::process::{Command, Stdio};
path::Path, use std::str::FromStr;
process::{Command, Stdio},
};
use thiserror::Error; use anyhow::{bail, format_err};
use fs_err as fs;
use fs_err::OpenOptions;
use structopt::StructOpt;
use crate::cli::{InitCommand, InitKind}; use super::resolve_path;
static MODEL_PROJECT: &str = static MODEL_PROJECT: &str =
include_str!("../../assets/default-model-project/default.project.json"); include_str!("../../assets/default-model-project/default.project.json");
@@ -20,37 +21,71 @@ static PLACE_PROJECT: &str =
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md"); 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 PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
#[derive(Debug, Error)] /// Initializes a new Rojo project.
enum Error { #[derive(Debug, StructOpt)]
#[error("A project file named default.project.json already exists in this folder")] pub struct InitCommand {
AlreadyExists, /// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = "")]
pub path: PathBuf,
#[error("git init failed")] /// The kind of project to create, 'place' or 'model'. Defaults to place.
GitInit, #[structopt(long, default_value = "place")]
pub kind: InitKind,
} }
pub fn init(options: InitCommand) -> Result<(), anyhow::Error> { impl InitCommand {
let base_path = options.absolute_path(); pub fn run(self) -> anyhow::Result<()> {
fs::create_dir_all(&base_path)?; let base_path = resolve_path(&self.path);
fs::create_dir_all(&base_path)?;
let canonical = fs::canonicalize(&base_path)?; let canonical = fs::canonicalize(&base_path)?;
let project_name = canonical let project_name = canonical
.file_name() .file_name()
.and_then(|name| name.to_str()) .and_then(|name| name.to_str())
.unwrap_or("new-project"); .unwrap_or("new-project");
let project_params = ProjectParams { let project_params = ProjectParams {
name: project_name.to_owned(), name: project_name.to_owned(),
}; };
match options.kind { match self.kind {
InitKind::Place => init_place(&base_path, project_params), InitKind::Place => init_place(&base_path, project_params)?,
InitKind::Model => init_model(&base_path, project_params), InitKind::Model => init_model(&base_path, project_params)?,
}
println!("Created project successfully.");
Ok(())
} }
} }
fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> { /// The templates we support for initializing a Rojo project.
eprintln!("Creating new place project '{}'", project_params.name); #[derive(Debug, Clone, Copy)]
pub enum InitKind {
/// A place that contains a baseplate.
Place,
/// An empty model, suitable for a library or plugin.
Model,
}
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),
_ => Err(format_err!(
"Invalid init kind '{}'. Valid kinds are: place, model",
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); let project_file = project_params.render_template(PLACE_PROJECT);
try_create_project(base_path, &project_file)?; try_create_project(base_path, &project_file)?;
@@ -88,13 +123,11 @@ fn init_place(base_path: &Path, project_params: ProjectParams) -> Result<(), any
let git_ignore = project_params.render_template(PLACE_GIT_IGNORE); let git_ignore = project_params.render_template(PLACE_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?; try_git_init(base_path, &git_ignore)?;
eprintln!("Created project successfully.");
Ok(()) Ok(())
} }
fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), anyhow::Error> { fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
eprintln!("Creating new model project '{}'", project_params.name); println!("Creating new model project '{}'", project_params.name);
let project_file = project_params.render_template(MODEL_PROJECT); let project_file = project_params.render_template(MODEL_PROJECT);
try_create_project(base_path, &project_file)?; try_create_project(base_path, &project_file)?;
@@ -111,8 +144,6 @@ fn init_model(base_path: &Path, project_params: ProjectParams) -> Result<(), any
let git_ignore = project_params.render_template(MODEL_GIT_IGNORE); let git_ignore = project_params.render_template(MODEL_GIT_IGNORE);
try_git_init(base_path, &git_ignore)?; try_git_init(base_path, &git_ignore)?;
eprintln!("Created project successfully.");
Ok(()) Ok(())
} }
@@ -138,7 +169,7 @@ fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> {
let status = Command::new("git").arg("init").current_dir(path).status()?; let status = Command::new("git").arg("init").current_dir(path).status()?;
if !status.success() { if !status.success() {
return Err(Error::GitInit.into()); bail!("git init failed: status code {:?}", status.code());
} }
} }
@@ -195,13 +226,15 @@ fn try_create_project(base_path: &Path, contents: &str) -> Result<(), anyhow::Er
let file_res = OpenOptions::new() let file_res = OpenOptions::new()
.write(true) .write(true)
.create_new(true) .create_new(true)
.open(project_path); .open(&project_path);
let mut file = match file_res { let mut file = match file_res {
Ok(file) => file, Ok(file) => file,
Err(err) => { Err(err) => {
return match err.kind() { return match err.kind() {
io::ErrorKind::AlreadyExists => Err(Error::AlreadyExists.into()), io::ErrorKind::AlreadyExists => {
bail!("Project file already exists: {}", project_path.display())
}
_ => Err(err.into()), _ => Err(err.into()),
} }
} }

View File

@@ -24,7 +24,7 @@ use thiserror::Error;
pub use self::build::*; pub use self::build::*;
pub use self::doc::*; pub use self::doc::*;
pub use self::fmt_project::FmtProjectCommand; pub use self::fmt_project::FmtProjectCommand;
pub use self::init::*; pub use self::init::{InitCommand, InitKind};
pub use self::plugin::*; pub use self::plugin::*;
pub use self::serve::*; pub use self::serve::*;
pub use self::upload::*; pub use self::upload::*;
@@ -102,7 +102,6 @@ pub struct ColorChoiceParseError {
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
pub enum Subcommand { pub enum Subcommand {
/// Creates a new Rojo project.
Init(InitCommand), Init(InitCommand),
/// Serves the project's files for use with the Rojo Studio plugin. /// Serves the project's files for use with the Rojo Studio plugin.
@@ -123,66 +122,6 @@ pub enum Subcommand {
Plugin(PluginCommand), Plugin(PluginCommand),
} }
/// Initializes a new Rojo project.
#[derive(Debug, StructOpt)]
pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = "")]
pub path: PathBuf,
/// The kind of project to create, 'place' or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
pub kind: InitKind,
}
impl InitCommand {
pub fn absolute_path(&self) -> Cow<'_, Path> {
resolve_path(&self.path)
}
}
/// The templates we support for initializing a Rojo project.
#[derive(Debug, Clone, Copy)]
pub enum InitKind {
/// A place that matches what File -> New does in Roblox Studio.
Place,
/// An empty model, suitable for a library or plugin.
Model,
}
impl FromStr for InitKind {
type Err = InitKindParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(InitKind::Place),
"model" => Ok(InitKind::Model),
_ => Err(InitKindParseError {
attempted: source.to_owned(),
}),
}
}
}
/// Error type for failing to parse an `InitKind`.
#[derive(Debug)]
pub struct InitKindParseError {
attempted: String,
}
impl Error for InitKindParseError {}
impl fmt::Display for InitKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Invalid init kind '{}'. Valid kinds are: place, model",
self.attempted
)
}
}
/// Expose a Rojo project through a web server that can communicate with the /// Expose a Rojo project through a web server that can communicate with the
/// Rojo Roblox Studio plugin, or be visited by the user in the browser. /// Rojo Roblox Studio plugin, or be visited by the user in the browser.
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]