1use std::path::Path;
33
34use anyhow::{Context, Result, bail};
35use clap::Args;
36use wildmatch::WildMatch;
37
38use super::util::{cmd, fs, workspace};
39use build_deps::build::kobo;
40use build_deps::versions;
41
42#[derive(Debug, Args)]
44pub struct DistArgs {
45 #[arg(long)]
47 pub test: bool,
48}
49
50pub fn run(args: DistArgs) -> Result<()> {
56 let root = workspace::root()?;
57
58 let binary = root.join("target/arm-unknown-linux-gnueabihf/release/cadmus");
59 if !binary.exists() {
60 bail!(
61 "ARM binary not found at {}.\n\
62 Run `cargo xtask build-kobo` first.",
63 binary.display()
64 );
65 }
66
67 let dist_dir = root.join("dist");
68 if dist_dir.exists() {
69 std::fs::remove_dir_all(&dist_dir).context("failed to remove existing dist/")?;
70 }
71 std::fs::create_dir_all(&dist_dir)?;
72 std::fs::create_dir_all(dist_dir.join("libs"))?;
73 std::fs::create_dir_all(dist_dir.join("dictionaries"))?;
74
75 copy_libraries(&root, &dist_dir)?;
76 copy_assets(&root, &dist_dir)?;
77 copy_binary(&root, &dist_dir)?;
78 strip_and_patch(&root, &dist_dir)?;
79 clean_user_files(&dist_dir)?;
80
81 if args.test {
82 println!("Test build assembled in dist/");
83 } else {
84 println!("Distribution assembled in dist/");
85 }
86
87 Ok(())
88}
89
90fn copy_libraries(root: &Path, dist_dir: &Path) -> Result<()> {
93 let libs_dir = root.join("libs");
94 let dist_libs = dist_dir.join("libs");
95
96 for &lib in versions::SONAMES {
97 let soname = kobo::soname(&libs_dir, lib)?;
98 let src = libs_dir.join(lib);
99 let dest = dist_libs.join(&soname);
100 std::fs::copy(&src, &dest).with_context(|| {
101 format!(
102 "failed to copy {} → {}\n\
103 Run `cargo xtask build-kobo` to build the libraries.",
104 src.display(),
105 dest.display()
106 )
107 })?;
108 }
109
110 Ok(())
111}
112
113fn copy_assets(root: &Path, dist_dir: &Path) -> Result<()> {
115 let dirs = [
116 "hyphenation-patterns",
117 "keyboard-layouts",
118 "bin",
119 "scripts",
120 "icons",
121 "resources",
122 "fonts",
123 "css",
124 ];
125
126 for dir in dirs {
127 let src = root.join(dir);
128 if !src.exists() {
129 bail!(
130 "Required asset directory '{}' not found.\n\
131 Run `cargo xtask download-assets` to download it.",
132 src.display()
133 );
134 }
135 fs::copy_dir_all(&src, &dist_dir.join(dir))?;
136 }
137
138 for entry in std::fs::read_dir(root.join("contrib"))? {
140 let entry = entry?;
141 let path = entry.path();
142 if path.extension().is_some_and(|e| e == "sh") {
143 std::fs::copy(&path, dist_dir.join(entry.file_name()))?;
144 }
145 }
146
147 std::fs::copy(
148 root.join("contrib/Settings-sample.toml"),
149 dist_dir.join("Settings-sample.toml"),
150 )?;
151
152 std::fs::copy(root.join("LICENSE"), dist_dir.join("LICENSE"))?;
153
154 Ok(())
155}
156
157fn copy_binary(root: &Path, dist_dir: &Path) -> Result<()> {
159 std::fs::copy(
160 root.join("target/arm-unknown-linux-gnueabihf/release/cadmus"),
161 dist_dir.join("cadmus"),
162 )
163 .context("failed to copy cadmus binary")?;
164 Ok(())
165}
166
167fn strip_and_patch(root: &Path, dist_dir: &Path) -> Result<()> {
172 let libs_dir = dist_dir.join("libs");
173 for entry in std::fs::read_dir(&libs_dir)? {
174 let path = entry?.path();
175 cmd::run(
176 "patchelf",
177 &["--remove-rpath", &path.to_string_lossy()],
178 root,
179 &[],
180 )?;
181 }
182
183 let binary = dist_dir.join("cadmus");
185 let mut strip_targets = vec![binary.to_string_lossy().into_owned()];
186 for entry in std::fs::read_dir(&libs_dir)? {
187 strip_targets.push(entry?.path().to_string_lossy().into_owned());
188 }
189
190 let strip_refs: Vec<&str> = strip_targets.iter().map(String::as_str).collect();
191 cmd::run("arm-linux-gnueabihf-strip", &strip_refs, root, &[])
192}
193
194fn clean_user_files(dist_dir: &Path) -> Result<()> {
196 let patterns: &[(&str, &str)] = &[
197 ("css", "*-user.css"),
198 ("keyboard-layouts", "*-user.json"),
199 ("hyphenation-patterns", "*.bounds"),
200 ("scripts", "wifi-*-*.sh"),
201 ];
202
203 for (subdir, pattern) in patterns {
204 let dir = dist_dir.join(subdir);
205 if dir.exists() {
206 remove_matching(&dir, pattern)?;
207 }
208 }
209
210 Ok(())
211}
212
213fn remove_matching(dir: &Path, pattern: &str) -> Result<()> {
215 let matcher = WildMatch::new(pattern);
216
217 for entry in std::fs::read_dir(dir)? {
218 let path = entry?.path();
219 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
220 if matcher.matches(name) {
221 std::fs::remove_file(&path)
222 .with_context(|| format!("failed to remove {}", path.display()))?;
223 }
224 }
225 }
226 Ok(())
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use std::fs;
233
234 #[test]
235 fn remove_matching_deletes_only_matching_entries() {
236 let tmp = tempfile::tempdir().unwrap();
237 let dir = tmp.path();
238
239 fs::write(dir.join("default-user.css"), b"x").unwrap();
240 fs::write(dir.join("default.css"), b"x").unwrap();
241 fs::write(dir.join("wifi-enable-eth0.sh"), b"x").unwrap();
242 fs::write(dir.join("wifi-enable.sh"), b"x").unwrap();
243
244 remove_matching(dir, "*-user.css").unwrap();
245 remove_matching(dir, "wifi-*-*.sh").unwrap();
246
247 assert!(!dir.join("default-user.css").exists());
248 assert!(dir.join("default.css").exists());
249 assert!(!dir.join("wifi-enable-eth0.sh").exists());
250 assert!(dir.join("wifi-enable.sh").exists());
251 }
252}