1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
use crate::errors::*;
use crate::extraction::extract_dir;
#[allow(unused_imports)]
use crate::PERSEUS_VERSION;
use cargo_toml::Manifest;
use include_dir::{include_dir, Dir};
use std::env;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;

// This literally includes the entire subcrate in the program, allowing more efficient development.
// This MUST be copied in from `../../examples/cli/.perseus/` every time the CLI is tested (use the Bonnie script).
const SUBCRATES: Dir = include_dir!("./.perseus");

/// Prepares the user's project by copying in the `.perseus/` subcrates. We use these subcrates to do all the building/serving, we just
/// have to execute the right commands in the CLI. We can essentially treat the subcrates themselves as a blackbox of just a folder.
pub fn prepare(dir: PathBuf) -> Result<(), PrepError> {
    // The location in the target directory at which we'll put the subcrates
    let mut target = dir;
    target.extend([".perseus"]);

    if target.exists() {
        // We don't care if it's corrupted etc., it just has to exist
        // If the user wants to clean it, they can do that
        // Besides, we want them to be able to customize stuff
        Ok(())
    } else {
        // Write the stored directory to that location, creating the directory first
        if let Err(err) = fs::create_dir(&target) {
            return Err(PrepError::ExtractionFailed {
                target_dir: target.to_str().map(|s| s.to_string()),
                source: err,
            });
        }
        // Notably, this function will not do anything or tell us if the directory already exists...
        if let Err(err) = extract_dir(SUBCRATES, &target) {
            return Err(PrepError::ExtractionFailed {
                target_dir: target.to_str().map(|s| s.to_string()),
                source: err,
            });
        }
        // 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!
        // We have to store `Cargo.toml` as `Cargo.toml.old` for packaging
        let root_manifest_pkg = target.join("Cargo.toml.old");
        let root_manifest = target.join("Cargo.toml");
        let server_manifest_pkg = target.join("server/Cargo.toml.old");
        let server_manifest = target.join("server/Cargo.toml");
        let builder_manifest_pkg = target.join("builder/Cargo.toml.old");
        let builder_manifest = target.join("builder/Cargo.toml");
        let root_manifest_contents = fs::read_to_string(&root_manifest_pkg).map_err(|err| {
            PrepError::ManifestUpdateFailed {
                target_dir: root_manifest_pkg.to_str().map(|s| s.to_string()),
                source: err,
            }
        })?;
        let server_manifest_contents = fs::read_to_string(&server_manifest_pkg).map_err(|err| {
            PrepError::ManifestUpdateFailed {
                target_dir: server_manifest_pkg.to_str().map(|s| s.to_string()),
                source: err,
            }
        })?;
        let builder_manifest_contents =
            fs::read_to_string(&builder_manifest_pkg).map_err(|err| {
                PrepError::ManifestUpdateFailed {
                    target_dir: builder_manifest_pkg.to_str().map(|s| s.to_string()),
                    source: err,
                }
            })?;
        // 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| PrepError::GetUserManifestFailed { source: err })?;
        let user_crate_name = user_manifest.package;
        let user_crate_name = match user_crate_name {
            Some(package) => package.name,
            None => return Err(PrepError::MalformedUserManifest),
        };
        // Update the name of the user's crate (Cargo needs more than just a path and an alias)
        // We don't need to do that in the server manifest because it uses the root code (which does parsing after `define_app!`)
        // We used to add a workspace here, but that means size optimizations apply to both the client and the server, so that's not done anymore
        // Now, we use an empty workspace to make sure we don't ninclude the engine in any user workspaces
        let updated_root_manifest = root_manifest_contents
            .replace("perseus-example-basic", &user_crate_name)
            + "\n[workspace]";
        let updated_server_manifest = server_manifest_contents + "\n[workspace]";
        let updated_builder_manifest = builder_manifest_contents + "\n[workspace]";

        // If we're not in development, also update relative path references
        #[cfg(not(debug_assertions))]
        let updated_root_manifest = updated_root_manifest.replace(
            "path = \"../../../packages/perseus\"",
            &format!("version = \"{}\"", PERSEUS_VERSION),
        );
        #[cfg(not(debug_assertions))]
        let updated_server_manifest = updated_server_manifest
            .replace(
                "path = \"../../../../packages/perseus\"",
                &format!("version = \"{}\"", PERSEUS_VERSION),
            )
            .replace(
                "path = \"../../../../packages/perseus-actix-web\"",
                &format!("version = \"{}\"", PERSEUS_VERSION),
            )
            .replace(
                "path = \"../../../../packages/perseus-warp\"",
                &format!("version = \"{}\"", PERSEUS_VERSION),
            );
        #[cfg(not(debug_assertions))]
        let updated_builder_manifest = updated_builder_manifest.replace(
            "path = \"../../../../packages/perseus\"",
            &format!("version = \"{}\"", PERSEUS_VERSION),
        );

        // Write the updated manifests back
        if let Err(err) = fs::write(&root_manifest, updated_root_manifest) {
            return Err(PrepError::ManifestUpdateFailed {
                target_dir: root_manifest.to_str().map(|s| s.to_string()),
                source: err,
            });
        }
        if let Err(err) = fs::write(&server_manifest, updated_server_manifest) {
            return Err(PrepError::ManifestUpdateFailed {
                target_dir: server_manifest.to_str().map(|s| s.to_string()),
                source: err,
            });
        }
        if let Err(err) = fs::write(&builder_manifest, updated_builder_manifest) {
            return Err(PrepError::ManifestUpdateFailed {
                target_dir: builder_manifest.to_str().map(|s| s.to_string()),
                source: err,
            });
        }

        // 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/") {
                return Ok(());
            }
        }
        let file = OpenOptions::new()
            .append(true)
            .create(true) // If it doesn't exist, create it
            .open(".gitignore");
        let mut file = match file {
            Ok(file) => file,
            Err(err) => return Err(PrepError::GitignoreUpdateFailed { source: err }),
        };
        // Check for errors with appending to the file
        if let Err(err) = file.write_all(b"\n.perseus/") {
            return Err(PrepError::GitignoreUpdateFailed { source: err });
        }
        Ok(())
    }
}

/// Checks if the user has the necessary prerequisites on their system (i.e. `cargo` and `wasm-pack`). These can all be checked
/// by just trying to run their binaries and looking for errors. If the user has other paths for these, they can define them under the
/// environment variables `PERSEUS_CARGO_PATH` and `PERSEUS_WASM_PACK_PATH`.
pub fn check_env() -> Result<(), PrepError> {
    // We'll loop through each prerequisite executable to check their existence
    // If the spawn returns an error, it's considered not present, success means presence
    let prereq_execs = vec![
        (
            env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()),
            "cargo",
            "PERSEUS_CARGO_PATH",
        ),
        (
            env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()),
            "wasm-pack",
            "PERSEUS_WASM_PACK_PATH",
        ),
    ];

    for exec in prereq_execs {
        let res = Command::new(&exec.0).output();
        // Any errors are interpreted as meaning that the user doesn't have the prerequisite installed properly.
        if let Err(err) = res {
            return Err(PrepError::PrereqNotPresent {
                cmd: exec.1.to_string(),
                env_var: exec.2.to_string(),
                source: err,
            });
        }
    }

    Ok(())
}