terrazzo_build/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::ffi::OsStr;
4use std::path::Path;
5use std::path::PathBuf;
6
7use nameth::NamedEnumValues as _;
8use nameth::NamedType as _;
9use nameth::nameth;
10
11/// Options for [build].
12pub struct BuildOptions<'t> {
13    /// Where the client code is located. Usually this is
14    /// ```
15    /// std::env::var("CARGO_MANIFEST_DIR")
16    /// # ;
17    /// ```
18    pub client_dir: PathBuf,
19
20    /// Where the server code is located. Usually this is also
21    /// ```
22    /// std::env::var("CARGO_MANIFEST_DIR")
23    /// # ;
24    /// ```
25    pub server_dir: PathBuf,
26
27    /// A list of extra compile options.
28    ///
29    /// For example, to compile the client code with the `"client"` and `"max_level_info"` features enabled,
30    /// add `["--features", "client,max_level_info"]` to `wasm_pack_options`.
31    pub wasm_pack_options: &'t [&'t str],
32}
33
34/// A `cargo` build script helper, to compile the client code to WASM and copy assets to the target folder.
35///
36/// Uses [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/quickstart.html) to build the WASM assembly.
37pub fn build(options: BuildOptions) -> Result<(), BuildError> {
38    // https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargo-warning
39    // for (key, value) in std::env::vars() {
40    //     println!("cargo::warning={key} = {value}");
41    // }
42
43    let BuildOptions {
44        client_dir,
45        server_dir,
46        wasm_pack_options,
47    } = options;
48
49    // .../client/src
50    // https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed
51    let client_src_dir = client_dir
52        .join("src")
53        .to_str()
54        .ok_or(BuildErrorInner::InvalidClientSrcDir)?
55        .to_owned();
56    println!("cargo::rerun-if-changed={client_src_dir}");
57
58    // .../client/pkg
59    let client_pkg_dir = client_dir.join("pkg");
60
61    // rm -rf .../client/pkg
62    rm(&client_pkg_dir, BuildErrorInner::RmClientPkgError)?;
63
64    // cd .../client
65    // wasm-pack build --target web
66    let mut wasm_pack = std::process::Command::new("wasm-pack");
67    wasm_pack
68        .args(["build", "--target", "web"])
69        .args(wasm_pack_options)
70        .args(["--target-dir", "target/wasm"])
71        .current_dir(&client_dir);
72    for (key, value) in std::env::vars() {
73        if !key.starts_with("CARGO_") && key != "DEBUG" && key != "OPT_LEVEL" && key != "PROFILE" {
74            wasm_pack.env(key, value);
75        }
76    }
77    // for (key, value) in wasm_pack.get_envs() {
78    //     println! { "cargo::warning={key} = {value}", key = key.to_string_lossy(), value = value.unwrap().to_string_lossy() };
79    // }
80    let () = wasm_pack
81        .status()
82        .map_err(|_| BuildErrorInner::WasmPackError)?
83        .success()
84        .then_some(())
85        .ok_or(BuildErrorInner::WasmPackError)?;
86
87    // .../server/assets/wasm
88    let assets_dir = server_dir.join("assets");
89    let assets_wasm_dir = assets_dir.join("wasm");
90
91    // rm -rf .../server/assets/wasm
92    rm(&assets_wasm_dir, BuildErrorInner::RmServerAssetsWasmError)?;
93
94    mv(
95        &client_pkg_dir,
96        &assets_wasm_dir,
97        BuildErrorInner::MvWasmError,
98    )?;
99
100    let cargo_manifest_dir =
101        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
102    let debug_or_release = if cfg!(debug_assertions) {
103        "debug"
104    } else {
105        "release"
106    };
107    let target_dir = cargo_manifest_dir.join("target").join(debug_or_release);
108    let target_asset_dir = target_dir.join("assets");
109    rm(&target_asset_dir, BuildErrorInner::RmTargetAssetsError)?;
110    mkdir(&target_dir, BuildErrorInner::MkdirTargetAssetsError)?;
111    cp(
112        &assets_dir,
113        &target_asset_dir,
114        BuildErrorInner::CpTargetAssetsError,
115    )?;
116
117    Ok(())
118}
119
120fn cp<E>(from: &Path, to: &Path, error: E) -> Result<(), E> {
121    let Ok(status) = std::process::Command::new("cp")
122        .args([OsStr::new("-R"), from.as_os_str(), to.as_os_str()])
123        .status()
124    else {
125        return Err(error);
126    };
127    status.success().then_some(()).ok_or(error)
128}
129
130fn mkdir<E>(path: &Path, error: E) -> Result<(), E> {
131    let Ok(status) = std::process::Command::new("mkdir")
132        .args([OsStr::new("-p"), path.as_os_str()])
133        .status()
134    else {
135        return Err(error);
136    };
137    status.success().then_some(()).ok_or(error)
138}
139
140fn mv<E>(from: &Path, to: &Path, error: E) -> Result<(), E> {
141    let Ok(status) = std::process::Command::new("mv")
142        .args([from.as_os_str(), to.as_os_str()])
143        .status()
144    else {
145        return Err(error);
146    };
147    status.success().then_some(()).ok_or(error)
148}
149
150fn rm<E>(path: &Path, error: E) -> Result<(), E> {
151    let Ok(status) = std::process::Command::new("rm")
152        .args([OsStr::new("-rf"), path.as_os_str()])
153        .status()
154    else {
155        return Err(error);
156    };
157    status.success().then_some(()).ok_or(error)
158}
159
160/// Errors returned by [build].
161#[nameth]
162#[derive(thiserror::Error, Debug)]
163#[error("[{t}] {0}", t = Self::type_name())]
164pub struct BuildError(#[from] BuildErrorInner);
165
166#[nameth]
167#[derive(thiserror::Error, Debug)]
168enum BuildErrorInner {
169    #[error("[{n}] Client src dir is invalid UTF-8", n = self.name())]
170    InvalidClientSrcDir,
171
172    #[error("[{n}] Failed to eraze old client pkg folder", n = self.name())]
173    RmClientPkgError,
174
175    #[error("[{n}] Failed build the WASM", n = self.name())]
176    WasmPackError,
177
178    #[error("[{n}] Failed to eraze server assets wasm folder", n = self.name())]
179    RmServerAssetsWasmError,
180
181    #[error("[{n}] Failed to move the wasm to the server assets folder", n = self.name())]
182    MvWasmError,
183
184    #[error("[{n}] Failed to erase the target assets folder", n = self.name())]
185    RmTargetAssetsError,
186
187    #[error("[{n}] Failed to make the target assets folder", n = self.name())]
188    MkdirTargetAssetsError,
189
190    #[error("[{n}] Failed to copy to the target assets folder", n = self.name())]
191    CpTargetAssetsError,
192}
193
194/// Invokes [stylance](https://crates.io/crates/stylance-cli) at compile time.
195pub fn build_css() {
196    let dir: PathBuf = std::env::var("CARGO_MANIFEST_DIR").unwrap().into();
197    let status = std::process::Command::new("stylance")
198        .current_dir(&dir)
199        .arg(".")
200        .status();
201    assert!(status.unwrap().success());
202}