Skip to content

Commit

Permalink
feat: ✨ set up cli systems for preparation and directory cleaning
Browse files Browse the repository at this point in the history
Still need to do `build` and `serve` commands.
  • Loading branch information
arctic-hen7 committed Aug 28, 2021
1 parent fd59eb3 commit 36660f8
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 17 deletions.
1 change: 1 addition & 0 deletions examples/cli/.perseus/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# This crate defines the user's app in terms that WASM can understand, making development significantly simpler.
# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it!

[package]
name = "perseus-cli-builder"
Expand Down
1 change: 1 addition & 0 deletions examples/cli/.perseus/server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# This crate defines the user's app in terms that WASM can understand, making development significantly simpler.
# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it!

[package]
name = "perseus-cli-server"
Expand Down
1 change: 1 addition & 0 deletions packages/perseus-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
include_dir = "0.6"
error-chain = "0.12"
cargo_toml = "0.9"

[lib]
name = "lib"
Expand Down
47 changes: 33 additions & 14 deletions packages/perseus-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::env;
use std::io::Write;
use lib::{PERSEUS_VERSION, help, check_env, prepare};
use std::path::PathBuf;
use lib::{PERSEUS_VERSION, help, check_env, prepare, delete_bad_dir, build, serve};
use lib::errors::*;

// All this does is run the program and terminate with the acquired exit code
Expand All @@ -15,13 +16,29 @@ fn main() {

// This manages error handling and returns a definite exit code to terminate with
fn real_main() -> i32 {
let res = core();
// Get the working directory
let dir = env::current_dir();
let dir = match dir {
Ok(dir) => dir,
Err(err) => {
let err = ErrorKind::CurrentDirUnavailable(err.to_string());
eprintln!("{}", err);
return 1
}
};
let res = core(dir.clone());
match res {
// If it worked, we pass the executed command's exit code through
Ok(exit_code) => exit_code,
// If something failed, we print the error to `stderr` and return a failure exit code
Err(err) => {
eprintln!("{}", err);
// Check if the error needs us to delete a partially-formed '.perseus/' directory
if err_should_cause_deletion(&err) {
if let Err(err) = delete_bad_dir(dir) {
eprintln!("{}", err);
}
}
1
}
}
Expand All @@ -31,19 +48,13 @@ fn real_main() -> i32 {
// This returns the exit code of the executed command, which we should return from the process itself
// This prints warnings using the `writeln!` macro, which allows the parsing of `stdout` in production or a vector in testing
// If at any point a warning can't be printed, the program will panic
fn core() -> Result<i32> {
fn core(dir: PathBuf) -> Result<i32> {
// Get `stdout` so we can write warnings appropriately
let stdout = &mut std::io::stdout();
// Get the arguments to this program, removing the first one (something like `perseus`)
let mut prog_args: Vec<String> = env::args().collect();
// This will panic if the first argument is not found (which is probably someone trying to fuzz us)
let _executable_name = prog_args.remove(0);
// Get the working directory
let dir = env::current_dir();
let dir = match dir {
Ok(dir) => dir,
Err(err) => bail!(ErrorKind::CurrentDirUnavailable(err.to_string()))
};
// Check the user's environment to make sure they have prerequisites
check_env()?;
// Check for special arguments
Expand All @@ -55,13 +66,21 @@ fn core() -> Result<i32> {
help(stdout);
Ok(0)
} else {
prepare(dir)?;
// Now we're checking commands
// Now we can check commands
if prog_args[0] == "build" {
// build(&prog_args, dir)
todo!("build command")
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
build(dir, &prog_args)?;
Ok(0)
} else if prog_args[0] == "serve" {
todo!("serve command")
// Set up the '.perseus/' directory if needed
prepare(dir.clone())?;
serve(dir, &prog_args)?;
Ok(0)
} else if prog_args[0] == "clean" {
// Just delete the '.perseus/' directory directly, as we'd do in a corruption
delete_bad_dir(dir)?;
Ok(0)
} else {
writeln!(stdout, "Unknown command '{}'. You can see the help page with -h/--help.", prog_args[0]);
Ok(1)
Expand Down
7 changes: 7 additions & 0 deletions packages/perseus-cli/src/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use std::path::PathBuf;
use crate::errors::*;

/// Builds the subcrates to get a directory that we can serve.
pub fn build(dir: PathBuf, prog_args: &[String]) -> Result<()> {
todo!("build command")
}
31 changes: 29 additions & 2 deletions packages/perseus-cli/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,39 @@ error_chain! {
// The `PathBuf` will be converted to a string, and unwrapping is bad in that context
ExtractionFailed(target_dir: Option<String>, err: String) {
description("subcrate extraction failed")
display("Couldn't extract internal subcrates to '{:?}'. You may not have the permissions necessary to write to this location, or the directory disappeared out from under the CLI. Error was: '{}'.", target_dir, err)
display("Couldn't extract internal subcrates to '{:?}'. You may not have the permissions necessary to write to this location, or the directory disappeared out from under the CLI. The '.perseus/' directory has been automatically deleted for safety. Error was: '{}'.", target_dir, err)
}
/// For when updating the user's .gitignore fails
GitignoreUpdateFailed(err: String) {
description("updating gitignore failed")
display("Couldn't update your .gitignore file to ignore the Perseus subcrates. Error was: '{}'. In the meantime, please manually add 'perseus/' to your .gitignore.", err)
display("Couldn't update your .gitignore file to ignore the Perseus subcrates. The '.perseus/' directory has been automatically deleted (necessary further steps not executed). Error was: '{}'.", err)
}
/// For when updating relative paths and package names in the manifest failed.
ManifestUpdateFailed(target: Option<String>, err: String) {
description("updating manifests failed")
display("Couldn't update internal manifest file at '{:?}'. If the error persists, make sure you have file write permissions. The '.perseus/' directory has been automatically deleted. Error was: '{}'.", target, err)
}
/// For when we can't get the user's `Cargo.toml` file.
GetUserManifestFailed(err: String) {
description("reading user manifest failed")
display("Couldn't read your crate's manifest (Cargo.toml) file. Please make sure this file exists, is valid, and that you're running Perseus in the right directory.The '.perseus/' directory has been automatically deleted. Error was: '{}'.", err)
}
/// For when a partially-formed '.perseus/' directory couldn't be removed, but did exist.
RemoveBadDirFailed(target: Option<String>, err: String) {
description("removing corrupted '.perseus/' directory failed")
display("Couldn't remove '.perseus/' directory at '{:?}'. Please remove the '.perseus/' directory manually (particularly if you didn't intentionally run the 'clean' command, that means the directory has been corrupted). Error was: '{}'.", target, err)

}
}
}

/// Checks if the given error should cause the CLI to delete the '.perseus/' folder so the user doesn't have something incomplete.
/// When deleting the directory, it should only be deleted if it exists, if not don't worry. If it does and deletion fails, fail like hell.
pub fn err_should_cause_deletion(err: &Error) -> bool {
matches!(
err.kind(),
ErrorKind::ExtractionFailed(_, _) |
ErrorKind::GitignoreUpdateFailed(_) |
ErrorKind::ManifestUpdateFailed(_, _)
)
}
24 changes: 23 additions & 1 deletion packages/perseus-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
mod help;
mod prepare;
pub mod errors;
mod build;
mod serve;

pub const PERSEUS_VERSION: &str = "0.1.0";
use errors::*;
use std::fs;
use std::path::PathBuf;

pub const PERSEUS_VERSION: &str = env!("CARGO_PKG_VERSION");
pub use help::help;
pub use prepare::{prepare, check_env};
pub use build::build;
pub use serve::serve;

/// Deletes a corrupted '.perseus/' directory. This qwill be called on certain error types that would leave the user with a half-finished
/// product, which is better to delete for safety and sanity.
pub fn delete_bad_dir(dir: PathBuf) -> Result<()> {
let mut target = dir;
target.extend([".perseus"]);
// We'll only delete the directory if it exists, otherwise we're fine
if target.exists() {
if let Err(err) = fs::remove_dir_all(&target) {
bail!(ErrorKind::RemoveBadDirFailed(target.to_str().map(|s| s.to_string()), err.to_string()))
}
}
Ok(())
}
37 changes: 37 additions & 0 deletions packages/perseus-cli/src/prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use std::fs;
use std::io::Write;
use std::fs::OpenOptions;
use std::process::Command;
use cargo_toml::Manifest;
use crate::errors::*;
use crate::PERSEUS_VERSION;

/// This literally includes the entire subcrate in the program, allowing more efficient development.
const SUBCRATES: Dir = include_dir!("../../examples/cli/.perseus");
Expand Down Expand Up @@ -33,6 +35,41 @@ pub fn prepare(dir: PathBuf) -> Result<()> {
if let Err(err) = SUBCRATES.extract(&target) {
bail!(ErrorKind::ExtractionFailed(target.to_str().map(|s| s.to_string()), err.to_string()))
}
// Use the current version of this crate (and thus all Perseus crates) to replace the relative imports
// That way everything works in dev and in prod on another system!
let mut root_manifest = target.clone();
root_manifest.extend(["Cargo.toml"]);
let mut server_manifest = target.clone();
server_manifest.extend(["server", "Cargo.toml"]);
let root_manifest_contents = fs::read_to_string(&root_manifest)
.map_err(|err| ErrorKind::ManifestUpdateFailed(root_manifest.to_str().map(|s| s.to_string()), err.to_string()))?;
let server_manifest_contents = fs::read_to_string(&server_manifest)
.map_err(|err| ErrorKind::ManifestUpdateFailed(server_manifest.to_str().map(|s| s.to_string()), err.to_string()))?;
// Get the name of the user's crate (which the subcrates depend on)
// We assume they're running this in a folder with a Cargo.toml...
let user_manifest = Manifest::from_path("./Cargo.toml")
.map_err(|err| ErrorKind::GetUserManifestFailed(err.to_string()))?;
let user_crate_name = user_manifest.package;
let user_crate_name = match user_crate_name {
Some(package) => package.name,
None => bail!(ErrorKind::GetUserManifestFailed("no '[package]' section in manifest".to_string()))
};
// Replace the relative path references to Perseus packages
// Also update the name of the user's crate (Cargo needs more than just a path and an alias)
let updated_root_manifest = root_manifest_contents
.replace("{ path = \"../../../packages/perseus\" }", &format!("\"{}\"", PERSEUS_VERSION))
.replace("perseus-example-cli", &user_crate_name);
let updated_server_manifest = server_manifest_contents
.replace("{ path = \"../../../../packages/perseus-actix-web\" }", &format!("\"{}\"", PERSEUS_VERSION))
.replace("perseus-example-cli", &user_crate_name);
// Write the updated manifests back
if let Err(err) = fs::write(&root_manifest, updated_root_manifest) {
bail!(ErrorKind::ManifestUpdateFailed(root_manifest.to_str().map(|s| s.to_string()), err.to_string()))
}
if let Err(err) = fs::write(&server_manifest, updated_server_manifest) {
bail!(ErrorKind::ManifestUpdateFailed(server_manifest.to_str().map(|s| s.to_string()), err.to_string()))
}

// If we aren't already gitignoring the subcrates, update .gitignore to do so
if let Ok(contents) = fs::read_to_string(".gitignore") {
if contents.contains(".perseus/") {
Expand Down
9 changes: 9 additions & 0 deletions packages/perseus-cli/src/serve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use std::path::PathBuf;
use crate::errors::*;

/// Serves the user's app. If no arguments are provided, this will build in watch mode and serve. If `-p/--prod` is specified, we'll
/// build for development, and if `--no-build` is specified, we won't build at all (useful for pseudo-production serving).
/// General message though: do NOT use the CLI for production serving!
pub fn serve(dir: PathBuf, prog_args: &[String]) -> Result<()> {
todo!("serve command")
}

0 comments on commit 36660f8

Please sign in to comment.