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