From 36660f899d0dc2dd389173b1299de36f4fa3c8dc Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Sat, 28 Aug 2021 19:59:26 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20set=20up=20cli=20systems=20?= =?UTF-8?q?for=20preparation=20and=20directory=20cleaning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Still need to do `build` and `serve` commands. --- examples/cli/.perseus/Cargo.toml | 1 + examples/cli/.perseus/server/Cargo.toml | 1 + packages/perseus-cli/Cargo.toml | 1 + packages/perseus-cli/src/bin/main.rs | 47 +++++++++++++++++-------- packages/perseus-cli/src/build.rs | 7 ++++ packages/perseus-cli/src/errors.rs | 31 ++++++++++++++-- packages/perseus-cli/src/lib.rs | 24 ++++++++++++- packages/perseus-cli/src/prepare.rs | 37 +++++++++++++++++++ packages/perseus-cli/src/serve.rs | 9 +++++ 9 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 packages/perseus-cli/src/build.rs create mode 100644 packages/perseus-cli/src/serve.rs diff --git a/examples/cli/.perseus/Cargo.toml b/examples/cli/.perseus/Cargo.toml index 7dfbbcb9b6..3518df8c60 100644 --- a/examples/cli/.perseus/Cargo.toml +++ b/examples/cli/.perseus/Cargo.toml @@ -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" diff --git a/examples/cli/.perseus/server/Cargo.toml b/examples/cli/.perseus/server/Cargo.toml index 8fee82b83f..838007d96c 100644 --- a/examples/cli/.perseus/server/Cargo.toml +++ b/examples/cli/.perseus/server/Cargo.toml @@ -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" diff --git a/packages/perseus-cli/Cargo.toml b/packages/perseus-cli/Cargo.toml index 0ed2044e99..b3a6cca29a 100644 --- a/packages/perseus-cli/Cargo.toml +++ b/packages/perseus-cli/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] include_dir = "0.6" error-chain = "0.12" +cargo_toml = "0.9" [lib] name = "lib" diff --git a/packages/perseus-cli/src/bin/main.rs b/packages/perseus-cli/src/bin/main.rs index efc1ded507..d72aa532da 100644 --- a/packages/perseus-cli/src/bin/main.rs +++ b/packages/perseus-cli/src/bin/main.rs @@ -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 @@ -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 } } @@ -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 { +fn core(dir: PathBuf) -> Result { // 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 = 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 @@ -55,13 +66,21 @@ fn core() -> Result { 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) diff --git a/packages/perseus-cli/src/build.rs b/packages/perseus-cli/src/build.rs new file mode 100644 index 0000000000..1edeaf09c1 --- /dev/null +++ b/packages/perseus-cli/src/build.rs @@ -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") +} diff --git a/packages/perseus-cli/src/errors.rs b/packages/perseus-cli/src/errors.rs index ef77947df7..7e07eb06cc 100644 --- a/packages/perseus-cli/src/errors.rs +++ b/packages/perseus-cli/src/errors.rs @@ -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, 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, 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, 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(_, _) + ) +} diff --git a/packages/perseus-cli/src/lib.rs b/packages/perseus-cli/src/lib.rs index d0a939d7f8..06de44d866 100644 --- a/packages/perseus-cli/src/lib.rs +++ b/packages/perseus-cli/src/lib.rs @@ -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(()) +} diff --git a/packages/perseus-cli/src/prepare.rs b/packages/perseus-cli/src/prepare.rs index 8a67858b46..92f96dd462 100644 --- a/packages/perseus-cli/src/prepare.rs +++ b/packages/perseus-cli/src/prepare.rs @@ -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"); @@ -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/") { diff --git a/packages/perseus-cli/src/serve.rs b/packages/perseus-cli/src/serve.rs new file mode 100644 index 0000000000..5f1fd40f73 --- /dev/null +++ b/packages/perseus-cli/src/serve.rs @@ -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") +}