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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
use crate::cmd::run_cmd_directly;
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, engine_url: &str) -> Result<(), PrepError> {
    // The location in the target directory at which we'll put the subcrates
    let target = dir.join(".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 {
        // Create 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,
            });
        }
        // Check if we're using the bundled engine or a custom one
        if engine_url == "default" {
            // Write the stored directory to the target location
            // 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,
                });
            }
        } else {
            // We're using a non-standard engine, which we'll download using Git
            // All other steps of integration with the user's package after this are the same
            let url_parts = engine_url.split('@').collect::<Vec<&str>>();
            let engine_url = url_parts[0];
            // A custom branch can be specified after a `@`, or we'll use `stable`
            let engine_branch = url_parts.get(1).unwrap_or(&"stable");
            let cmd = format!(
                // We'll only clone the production branch, and only the top level, we don't need the whole shebang
                "{} clone --single-branch --branch {branch} --depth 1 {repo} {output}",
                env::var("PERSEUS_GIT_PATH").unwrap_or_else(|_| "git".to_string()),
                branch = engine_branch,
                repo = engine_url,
                output = target.to_string_lossy()
            );
            println!("Fetching custom engine with command: '{}'.", &cmd);
            // Tell the user what command we're running so that they can debug it
            let exit_code = run_cmd_directly(
                cmd,
                &dir, // We'll run this in the current directory and output into `.perseus/`
            )
            .map_err(|err| PrepError::GetEngineFailed { source: err })?;
            if exit_code != 0 {
                return Err(PrepError::GetEngineNonZeroExitCode { exit_code });
            }
            // Now delete the Git internals
            let git_target = target.join(".git");
            if let Err(err) = fs::remove_dir_all(&git_target) {
                return Err(PrepError::RemoveEngineGitFailed {
                    target_dir: git_target.to_str().map(|s| s.to_string()),
                    source: err,
                });
            }
        }

        // Prepare for transformations on the manifest files
        // 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 re-exports the `PerseusApp`)
        // 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 include the engine in any user workspaces
        // We use a token here that's set by the build script
        let updated_root_manifest =
            root_manifest_contents.replace("USER_PKG_NAME", &user_crate_name) + "\n[workspace]";
        let updated_server_manifest = server_manifest_contents + "\n[workspace]";
        let updated_builder_manifest = builder_manifest_contents + "\n[workspace]";

        // We also need to set the Perseus version
        // In production, we'll use the full version, but in development we'll use relative path references from the examples
        // The tokens here are set by the build script once again
        // Production
        #[cfg(not(debug_assertions))]
        let updated_root_manifest = updated_root_manifest.replace(
            "PERSEUS_VERSION",
            &format!("version = \"{}\"", PERSEUS_VERSION),
        );
        #[cfg(not(debug_assertions))]
        let updated_server_manifest = updated_server_manifest
            .replace(
                "PERSEUS_VERSION",
                &format!("version = \"{}\"", PERSEUS_VERSION),
            )
            .replace(
                "PERSEUS_ACTIX_WEB_VERSION",
                &format!("version = \"{}\"", PERSEUS_VERSION),
            )
            .replace(
                "PERSEUS_WARP_VERSION",
                &format!("version = \"{}\"", PERSEUS_VERSION),
            );
        #[cfg(not(debug_assertions))]
        let updated_builder_manifest = updated_builder_manifest.replace(
            "PERSEUS_VERSION",
            &format!("version = \"{}\"", PERSEUS_VERSION),
        );
        // Development
        #[cfg(debug_assertions)]
        let updated_root_manifest = updated_root_manifest
            .replace("PERSEUS_VERSION", "path = \"../../../../packages/perseus\"");
        #[cfg(debug_assertions)]
        let updated_server_manifest = updated_server_manifest
            .replace(
                "PERSEUS_VERSION",
                "path = \"../../../../../packages/perseus\"",
            )
            .replace(
                "PERSEUS_ACTIX_WEB_VERSION",
                "path = \"../../../../../packages/perseus-actix-web\"",
            )
            .replace(
                "PERSEUS_WARP_VERSION",
                "path = \"../../../../../packages/perseus-warp\"",
            );
        #[cfg(debug_assertions)]
        let updated_builder_manifest = updated_builder_manifest.replace(
            "PERSEUS_VERSION",
            "path = \"../../../../../packages/perseus\"",
        );

        // 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(())
}