1use std::env;
2use std::path::{Path, PathBuf};
3use std::process::ExitCode;
4
5use sofie_check::{Screens, report};
6use sofie_engine::{Components, TranslationError, Translations, compile_template_with_texts};
7use sofie_model::App;
8use sofie_model::naming::{SOURCE_LANGUAGE, file_stem};
9
10use crate::cargo::Diagnostics;
11use crate::check_command::check_loaded_app;
12use crate::clients::{build_clients, client_sources, has_clients};
13use crate::gen_command::{GenError, readable_screen};
14use crate::islands::{IslandMode, build_islands, has_islands, island_sources};
15use crate::source_edits::{read, write};
16use crate::wiring::{embedded_mails, embedded_screens};
17
18#[must_use]
19pub fn check_in_build_script(root: &Path) -> ExitCode {
20 for path in watched_paths(root) {
21 println!("cargo::rerun-if-changed={}", path.display());
22 }
23 match build(root) {
24 Ok(()) => return ExitCode::SUCCESS,
25 Err(GenError::Check(findings)) => eprint!("{}", report(root, &findings)),
26 Err(error) => eprintln!("{error}"),
27 }
28 ExitCode::FAILURE
29}
30
31fn build(root: &Path) -> Result<(), GenError> {
32 let app = sofie_model::load(root).map_err(GenError::Model)?;
33 println!("{}", app_crate_variable(&app));
34 let screens = Screens::read(&app.modules);
35 let findings = check_loaded_app(root, &app, &screens)?;
36 if !findings.is_empty() {
37 return Err(GenError::Check(findings));
38 }
39 let Some(out) = env::var_os("OUT_DIR").map(PathBuf::from) else {
40 return Err(GenError::Usage(
41 "the screens compile in a build script, which Cargo gives OUT_DIR".to_owned(),
42 ));
43 };
44 let texts = source_texts(&app)?;
45 compile_screens(&app, &screens, &texts, &out.join("screens"))?;
46 compile_mails(&app, &texts, &out.join("mails"))?;
47 let mode = match env::var("PROFILE").as_deref() {
48 Ok("release") => IslandMode::Release,
49 _ => IslandMode::Development,
50 };
51 build_islands_into(&app, &out.join("islands"), mode)?;
52 build_clients_into(&app, &screens, &out.join("clients"), mode)
53}
54
55fn app_crate_variable(app: &App) -> String {
57 format!("cargo::rustc-env=SOFIE_APP_CRATE={}", app.app.crate_name)
58}
59
60fn source_texts(app: &App) -> Result<Translations, GenError> {
62 let refused = |error: TranslationError| {
63 GenError::App(format!(
64 "reading the app's `lang/{SOURCE_LANGUAGE}.toml` files: {error}"
65 ))
66 };
67 let mut texts = Translations::new(&[SOURCE_LANGUAGE]).map_err(refused)?;
68 for module in &app.modules {
69 let sources = module
70 .languages
71 .iter()
72 .filter(|path| file_stem(path) == SOURCE_LANGUAGE);
73 for path in sources {
74 texts
75 .add(&module.krate.crate_name, SOURCE_LANGUAGE, &read(path)?)
76 .map_err(refused)?;
77 }
78 }
79 Ok(texts)
80}
81
82fn compile_screens(
83 app: &App,
84 screens: &Screens,
85 texts: &Translations,
86 out: &Path,
87) -> Result<(), GenError> {
88 for (key, file) in app.modules.iter().flat_map(embedded_screens) {
89 let source = &readable_screen(screens, file)?.source;
90 let compiled = compile_template_with_texts(&key, source, screens.components(), texts)
91 .map_err(|error| GenError::Screen {
92 path: file.to_path_buf(),
93 message: error.to_string(),
94 })?;
95 write(&out.join(format!("{key}.sft")), &compiled)?;
96 }
97 Ok(())
98}
99
100fn compile_mails(app: &App, texts: &Translations, out: &Path) -> Result<(), GenError> {
101 for mail in embedded_mails(&app.modules) {
102 let source = read(&mail.file)?;
103 let compiled =
104 compile_template_with_texts(&mail.key, &source, &Components::default(), texts)
105 .map_err(|error| GenError::Screen {
106 path: mail.file.clone(),
107 message: error.to_string(),
108 })?;
109 write(&out.join(format!("{}.sft", mail.key)), &compiled)?;
110 }
111 Ok(())
112}
113
114fn build_islands_into(app: &App, out: &Path, mode: IslandMode) -> Result<(), GenError> {
115 if !has_islands(app) {
116 return Ok(());
117 }
118 for source in island_sources(app) {
119 println!("cargo::rerun-if-changed={}", source.display());
120 }
121 build_islands(app, out, mode, Diagnostics::Terminal).map(drop)
122}
123
124fn build_clients_into(
125 app: &App,
126 screens: &Screens,
127 out: &Path,
128 mode: IslandMode,
129) -> Result<(), GenError> {
130 if !has_clients(app) {
131 return Ok(());
132 }
133 for source in client_sources() {
134 println!("cargo::rerun-if-changed={}", source.display());
135 }
136 build_clients(app, screens, out, mode, Diagnostics::Terminal).map(drop)
137}
138
139fn watched_paths(root: &Path) -> Vec<PathBuf> {
142 let app = root.join("app");
143 let candidates = [
144 root.join("modules"),
145 app.join("Cargo.toml"),
146 app.join("src"),
147 app.join("tests"),
148 root.join(sofie_check::ALLOW_FILE),
149 ];
150 candidates
152 .into_iter()
153 .filter(|path| path.exists())
154 .collect()
155}
156
157#[cfg(test)]
158mod tests {
159 use std::path::Path;
160
161 use sofie_engine::{Components, compile_template_with_texts, load_template};
162 use sofie_model::testing::{TemporaryDirectory, app, module};
163
164 use super::{app_crate_variable, source_texts};
165
166 #[test]
167 fn the_build_script_names_the_app_s_crate_for_test_app() {
168 let shop = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/shop");
169 let app = sofie_model::load(&shop).unwrap();
170
171 assert_eq!(
172 app_crate_variable(&app),
173 "cargo::rustc-env=SOFIE_APP_CRATE=shop"
174 );
175 }
176
177 #[test]
178 fn screens_are_compiled_with_the_source_language_s_texts_only() {
179 let directory = TemporaryDirectory::new("sofie-source-texts");
180 let mut order = module("order", directory.path("modules/order"));
181 order.languages = vec![
182 directory.write(
183 "modules/order/lang/en.toml",
184 "[orders]\ntitle = \"Your orders\"\n",
185 ),
186 directory.write(
187 "modules/order/lang/tr.toml",
188 "[orders]\ntitle = \"Siparişleriniz\"\n",
189 ),
190 ];
191 let shop = app(directory.root(), vec![order]);
192
193 let texts = source_texts(&shop).unwrap();
194 let compiled = compile_template_with_texts(
195 "order/web/orders",
196 "<h1>{t(\"orders.title\")}</h1>",
197 &Components::default(),
198 &texts,
199 )
200 .unwrap();
201
202 let loaded = load_template("order/web/orders", &compiled).unwrap();
203 assert_eq!(
204 loaded.render(&serde_json::json!({})).unwrap(),
205 "<h1>Your orders</h1>"
206 );
207 }
208}