forked from rojo-rbx/rojo
226 lines
6.6 KiB
Rust
226 lines
6.6 KiB
Rust
use std::process::{Command, Stdio};
|
|
use std::str::FromStr;
|
|
use std::{
|
|
collections::VecDeque,
|
|
path::{Path, PathBuf},
|
|
};
|
|
use std::{
|
|
ffi::OsStr,
|
|
io::{self, Write},
|
|
};
|
|
|
|
use anyhow::{bail, format_err};
|
|
use clap::Parser;
|
|
use fs_err as fs;
|
|
use fs_err::OpenOptions;
|
|
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
|
|
|
|
use super::resolve_path;
|
|
|
|
const GIT_IGNORE_PLACEHOLDER: &str = "gitignore.txt";
|
|
|
|
static TEMPLATE_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/templates.bincode"));
|
|
|
|
/// Initializes a new Rojo project.
|
|
///
|
|
/// By default, this will attempt to initialize a 'git' repository in the
|
|
/// project directory if `git` is installed. To avoid this, pass `--skip-git`.
|
|
#[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'.
|
|
#[clap(long, default_value = "place")]
|
|
pub kind: InitKind,
|
|
|
|
/// Skips the initialization of a git repository.
|
|
#[clap(long)]
|
|
pub skip_git: bool,
|
|
}
|
|
|
|
impl InitCommand {
|
|
pub fn run(self) -> anyhow::Result<()> {
|
|
let template = self.kind.template();
|
|
|
|
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(),
|
|
};
|
|
|
|
println!(
|
|
"Creating new {:?} project '{}'",
|
|
self.kind, project_params.name
|
|
);
|
|
|
|
let vfs = Vfs::new(template);
|
|
vfs.set_watch_enabled(false);
|
|
|
|
let mut queue = VecDeque::with_capacity(8);
|
|
for entry in vfs.read_dir("")? {
|
|
queue.push_back(entry?.path().to_path_buf())
|
|
}
|
|
|
|
while let Some(mut path) = queue.pop_front() {
|
|
let metadata = vfs.metadata(&path)?;
|
|
if metadata.is_dir() {
|
|
fs_err::create_dir(base_path.join(&path))?;
|
|
for entry in vfs.read_dir(&path)? {
|
|
queue.push_back(entry?.path().to_path_buf());
|
|
}
|
|
} else {
|
|
let content = vfs.read_to_string_lf_normalized(&path)?;
|
|
if let Some(file_stem) = path.file_name().and_then(OsStr::to_str) {
|
|
if file_stem == GIT_IGNORE_PLACEHOLDER {
|
|
if self.skip_git {
|
|
continue;
|
|
} else {
|
|
path.set_file_name(".gitignore");
|
|
}
|
|
}
|
|
}
|
|
write_if_not_exists(
|
|
&base_path.join(&path),
|
|
&project_params.render_template(&content),
|
|
)?;
|
|
}
|
|
}
|
|
|
|
if !self.skip_git && should_git_init(&base_path) {
|
|
log::debug!("Initializing Git repository...");
|
|
|
|
let status = Command::new("git")
|
|
.arg("init")
|
|
.current_dir(&base_path)
|
|
.status()?;
|
|
|
|
if !status.success() {
|
|
bail!("git init failed: status code {:?}", status.code());
|
|
}
|
|
}
|
|
|
|
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 InitKind {
|
|
fn template(&self) -> InMemoryFs {
|
|
let template_path = match self {
|
|
Self::Place => "place",
|
|
Self::Model => "model",
|
|
Self::Plugin => "plugin",
|
|
};
|
|
|
|
let snapshot: VfsSnapshot = bincode::deserialize(TEMPLATE_BINCODE)
|
|
.expect("Rojo's templates were not properly packed into Rojo's binary");
|
|
|
|
if let VfsSnapshot::Dir { mut children } = snapshot {
|
|
if let Some(template) = children.remove(template_path) {
|
|
let mut fs = InMemoryFs::new();
|
|
fs.load_snapshot("", template)
|
|
.expect("loading a template in memory should never fail");
|
|
fs
|
|
} else {
|
|
panic!("template for project type {:?} is missing", self)
|
|
}
|
|
} else {
|
|
panic!("Rojo's templates were packed as a file instead of a directory")
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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"))
|
|
}
|
|
}
|
|
|
|
/// 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(())
|
|
}
|