Skip to main content

shuyi_cli/
watch.rs

1//! `run --watch`: the game's window program, in a debug build (so with its
2//! debug tools on), with the folder of its files on disk laid over the
3//! embedded ones and watched (ADR-0071). It isn't an operation of the
4//! contract: it plays nothing headless and answers nothing, and it runs
5//! until the player closes the window.
6
7use std::ffi::OsString;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use shuyi_platform::WATCH_VAR;
12
13use crate::contract::codes;
14use crate::export::package_and_program;
15use crate::project::{self, Project};
16
17/// The folder of a game's files when its project file doesn't say.
18const DEFAULT_ASSETS: &str = "assets";
19
20/// What `run --watch` starts.
21#[derive(Debug, PartialEq, Eq)]
22struct Plan {
23    /// `cargo`'s arguments.
24    args: Vec<String>,
25    /// The folder of the game's files.
26    assets: PathBuf,
27}
28
29/// What runs `bin` of `package` over the folder `assets` of the project at
30/// `root`, or why not.
31fn plan(root: &Path, assets: Option<&str>, package: &str, bin: &str) -> Result<Plan, String> {
32    let named = assets.unwrap_or(DEFAULT_ASSETS);
33    let assets = root.join(named);
34    if !assets.is_dir() {
35        return Err(format!(
36            "no folder {named} here: `assets` in {} names the folder of the game's files",
37            project::FILE
38        ));
39    }
40    Ok(Plan {
41        args: ["run", "-p", package, "--bin", bin]
42            .map(str::to_owned)
43            .to_vec(),
44        assets: std::path::absolute(&assets).unwrap_or(assets),
45    })
46}
47
48/// `run --watch` on `project`, `args` all it was given. Returns the exit
49/// code: the game's, as 0 or 1; 2 for another argument, 3 for a project that
50/// doesn't say what to run.
51pub fn main(project: &Project, args: &[String]) -> u8 {
52    let command = project.config.command();
53    if args.iter().any(|arg| arg != "--watch") {
54        let error = "--watch takes nothing else: it runs the game in its window";
55        let help = format!("usage: {command} run --watch");
56        eprint!(
57            "{}",
58            codes::said("run", codes::WRONG_ARGUMENTS.0, error, Some(&help))
59        );
60        return 2;
61    }
62    let config = &project.config;
63    let planned = package_and_program(&project.root, config)
64        .map_err(|e| e.message())
65        .and_then(|(package, bin)| plan(&project.root, config.assets.as_deref(), &package, &bin));
66    let plan = match planned {
67        Ok(plan) => plan,
68        Err(error) => {
69            return crate::contract::cli::setup_failed("run", args, codes::PROJECT_FILE, &error);
70        }
71    };
72    run(&plan, &project.root, crate::cargo())
73}
74
75/// Runs `plan` with `cargo` until the game quits.
76fn run(plan: &Plan, root: &Path, cargo: OsString) -> u8 {
77    println!(
78        "watching {}: a file changed there reloads in the game",
79        plan.assets.display()
80    );
81    let status = Command::new(cargo)
82        .args(&plan.args)
83        .current_dir(root)
84        .env(WATCH_VAR, &plan.assets)
85        .status();
86    match status {
87        Ok(status) if status.success() => 0,
88        Ok(status) => {
89            eprintln!(
90                "run --watch: cargo {} exited with {status}",
91                plan.args.join(" ")
92            );
93            1
94        }
95        Err(e) => {
96            let error = format!("cargo doesn't start: {e}");
97            eprint!(
98                "{}",
99                codes::said(
100                    "run",
101                    codes::TOOLCHAIN.0,
102                    &error,
103                    codes::help_of(codes::TOOLCHAIN.0)
104                )
105            );
106            3
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use std::fs;
114
115    use super::*;
116    use crate::project::Config;
117
118    /// An empty folder of its own for the test `name`.
119    fn folder(name: &str) -> PathBuf {
120        let dir = std::env::temp_dir().join(format!("shuyi-watch-{name}-{}", std::process::id()));
121        let _ = fs::remove_dir_all(&dir);
122        fs::create_dir_all(&dir).unwrap_or_else(|e| panic!("{e:?}"));
123        dir
124    }
125
126    #[test]
127    fn it_runs_the_program_over_the_folder_the_project_names() {
128        let dir = folder("names");
129        fs::create_dir(dir.as_path().join("files")).unwrap();
130        let planned = plan(dir.as_path(), Some("files"), "game", "game").unwrap();
131        assert_eq!(planned.args, ["run", "-p", "game", "--bin", "game"]);
132        assert!(planned.assets.ends_with("files"), "{planned:?}");
133        assert!(planned.assets.is_absolute(), "{planned:?}");
134    }
135
136    #[test]
137    fn the_folder_is_assets_unless_the_project_says() {
138        let dir = folder("default");
139        let error = plan(dir.as_path(), None, "game", "game").unwrap_err();
140        assert!(error.contains("no folder assets"), "{error}");
141        assert!(error.contains("shuyi.ron"), "{error}");
142        fs::create_dir(dir.as_path().join("assets")).unwrap();
143        let planned = plan(dir.as_path(), None, "game", "game").unwrap();
144        assert!(planned.assets.ends_with("assets"), "{planned:?}");
145    }
146
147    #[test]
148    fn it_takes_no_other_argument_and_needs_a_program() {
149        let dir = folder("args");
150        let project = Project::new(dir.as_path(), Config::default());
151        let args = ["--watch".to_owned(), "--frames".to_owned()];
152        assert_eq!(main(&project, &args), 2);
153        assert_eq!(main(&project, &["--watch".to_owned()]), 3);
154    }
155
156    #[test]
157    fn a_game_that_fails_is_a_failure_and_no_cargo_a_setup_to_fix() {
158        let dir = folder("fails");
159        let planned = Plan {
160            args: vec!["--no-such-flag".to_owned()],
161            assets: dir.as_path().to_path_buf(),
162        };
163        assert_eq!(run(&planned, dir.as_path(), crate::cargo()), 1);
164        let none = OsString::from("no-such-cargo-here");
165        assert_eq!(run(&planned, dir.as_path(), none), 3);
166    }
167}