|
1 | | -use std::{ |
2 | | - env, fs, io, |
3 | | - path::{Path, PathBuf}, |
4 | | -}; |
5 | | - |
6 | | -fn download_to(url: &str, dest: &Path) -> Result<(), String> { |
7 | | - println!("cargo:warning=Downloading {} -> {}", url, dest.display()); |
8 | | - if let Some(parent) = dest.parent() { |
9 | | - fs::create_dir_all(parent).map_err(|e| e.to_string())?; |
10 | | - } |
11 | | - let resp = reqwest::blocking::get(url).map_err(|e| e.to_string())?; |
12 | | - if !resp.status().is_success() { |
13 | | - return Err(format!("Request failed: {}", resp.status())); |
14 | | - } |
15 | | - let bytes = resp.bytes().map_err(|e| e.to_string())?; |
16 | | - fs::write(dest, &bytes).map_err(|e| e.to_string())?; |
17 | | - Ok(()) |
18 | | -} |
19 | | - |
20 | | -fn ensure_yt_dlp(target_os: &str, out_dir: &Path) -> Result<PathBuf, String> { |
21 | | - let (url, filename) = match target_os { |
22 | | - "windows" => ("https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe", "yt-dlp.exe"), |
23 | | - "macos" => ("https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos", "yt-dlp"), |
24 | | - _ => ("https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp", "yt-dlp"), // linux & others |
25 | | - }; |
26 | | - let dest = out_dir.join(filename); |
27 | | - if !dest.exists() { |
28 | | - download_to(url, &dest)?; |
29 | | - } |
30 | | - #[cfg(unix)] |
31 | | - { |
32 | | - use std::os::unix::fs::PermissionsExt; |
33 | | - if let Ok(meta) = fs::metadata(&dest) { |
34 | | - let mut perm = meta.permissions(); |
35 | | - perm.set_mode(0o755); |
36 | | - let _ = fs::set_permissions(&dest, perm); |
37 | | - } |
38 | | - } |
39 | | - Ok(dest) |
40 | | -} |
41 | | - |
42 | | -fn ensure_ffmpeg(target_os: &str, out_dir: &Path) -> Result<PathBuf, String> { |
43 | | - if target_os == "macos" { |
44 | | - return Err("Skipping ffmpeg embed on macOS".into()); |
45 | | - } |
46 | | - |
47 | | - let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_else(|_| "x86_64".into()); |
48 | | - let (platform_tag, is_zip) = match (target_os, target_arch.as_str()) { |
49 | | - ("windows", "aarch64") => ("winarm64", true), |
50 | | - ("windows", _) => ("win64", true), |
51 | | - ("linux", "aarch64") => ("linuxarm64", false), |
52 | | - ("linux", _) => ("linux64", false), |
53 | | - (other, _) => return Err(format!("Unsupported OS for ffmpeg embedding: {other}")), |
54 | | - }; |
55 | | - let archive_name = if is_zip { "ffmpeg.zip" } else { "ffmpeg.tar.xz" }; |
56 | | - let archive_url = format!( |
57 | | - "https://github.com/BtbN/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-{platform_tag}-gpl.{}", |
58 | | - if is_zip { "zip" } else { "tar.xz" } |
59 | | - ); |
60 | | - let bin_subpath = if target_os == "windows" { |
61 | | - format!("ffmpeg-master-latest-{platform_tag}-gpl/bin/ffmpeg.exe") |
62 | | - } else { |
63 | | - format!("ffmpeg-master-latest-{platform_tag}-gpl/bin/ffmpeg") |
64 | | - }; |
65 | | - let ffmpeg_bin = out_dir.join(if target_os == "windows" { "ffmpeg.exe" } else { "ffmpeg" }); |
66 | | - if ffmpeg_bin.exists() { |
67 | | - // Assume prior full extraction already happened. |
68 | | - return Ok(ffmpeg_bin); |
69 | | - } |
70 | | - |
71 | | - let archive_path = out_dir.join(archive_name); |
72 | | - download_to(&archive_url, &archive_path)?; |
73 | | - if is_zip { |
74 | | - let file = fs::File::open(&archive_path).map_err(|e| e.to_string())?; |
75 | | - let mut zip = zip::ZipArchive::new(file).map_err(|e| e.to_string())?; |
76 | | - for i in 0..zip.len() { |
77 | | - let mut f = zip.by_index(i).map_err(|e| e.to_string())?; |
78 | | - let name = f.name().to_string(); |
79 | | - if let Some(bin_prefix) = bin_subpath.rsplit_once('/') { |
80 | | - // (dir, file) |
81 | | - let bin_dir_prefix = bin_prefix.0.to_string(); |
82 | | - if name.ends_with('/') { |
83 | | - continue; |
84 | | - } |
85 | | - if name.contains(&bin_dir_prefix) { |
86 | | - if let Some(filename) = name.split('/').last() { |
87 | | - let out_path = out_dir.join(filename); |
88 | | - let mut out_f = fs::File::create(&out_path).map_err(|e| e.to_string())?; |
89 | | - io::copy(&mut f, &mut out_f).map_err(|e| e.to_string())?; |
90 | | - } |
91 | | - } |
92 | | - } |
93 | | - } |
94 | | - } else { |
95 | | - // tar.xz |
96 | | - let file = fs::File::open(&archive_path).map_err(|e| e.to_string())?; |
97 | | - let decompressor = xz2::read::XzDecoder::new(file); |
98 | | - let mut archive = tar::Archive::new(decompressor); |
99 | | - for entry in archive.entries().map_err(|e| e.to_string())? { |
100 | | - let mut entry = entry.map_err(|e| e.to_string())?; |
101 | | - if let Ok(path) = entry.path() { |
102 | | - if let Some(path_str) = path.to_str() { |
103 | | - if let Some((bin_dir_prefix, _file)) = bin_subpath.rsplit_once('/') { |
104 | | - if path_str.contains(bin_dir_prefix) && !path_str.ends_with('/') { |
105 | | - if let Some(filename) = path.file_name() { |
106 | | - let out_path = out_dir.join(filename); |
107 | | - entry.unpack(&out_path).map_err(|e| e.to_string())?; |
108 | | - } |
109 | | - } |
110 | | - } |
111 | | - } |
112 | | - } |
113 | | - } |
114 | | - } |
115 | | - let _ = fs::remove_file(&archive_path); |
116 | | - #[cfg(unix)] |
117 | | - { |
118 | | - use std::os::unix::fs::PermissionsExt; |
119 | | - if let Ok(meta) = fs::metadata(&ffmpeg_bin) { |
120 | | - let mut perm = meta.permissions(); |
121 | | - perm.set_mode(0o755); |
122 | | - let _ = fs::set_permissions(&ffmpeg_bin, perm); |
123 | | - } |
124 | | - } |
125 | | - Ok(ffmpeg_bin) |
126 | | -} |
| 1 | +use std::env; |
| 2 | +use std::path::Path; |
127 | 3 |
|
128 | 4 | fn main() { |
129 | | - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_else(|_| String::from("unknown")); |
130 | | - println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS"); |
131 | 5 | println!("cargo:rerun-if-changed=build.rs"); |
132 | | - println!("cargo:rustc-check-cfg=cfg(has_embedded_bins)"); |
133 | | - |
| 6 | + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_else(|_| String::from("unknown")); |
134 | 7 | if target_os == "windows" { |
135 | 8 | println!("cargo:rerun-if-changed=assets/app/avatar.ico"); |
136 | 9 | println!("cargo:rerun-if-changed=assets/app/icon.ico"); |
137 | 10 | let mut res = winres::WindowsResource::new(); |
| 11 | + |
138 | 12 | for path in ["assets/app/icon.ico", "assets/app/avatar.ico"] { |
139 | 13 | if Path::new(path).exists() { |
140 | 14 | res.set_icon(path); |
141 | | - let _ = res.compile(); |
142 | 15 | break; |
143 | 16 | } |
144 | 17 | } |
145 | | - } |
146 | | - |
147 | | - let bin_dir = Path::new("assets").join("bin"); |
148 | | - fs::create_dir_all(&bin_dir).expect("create bin dir"); |
149 | | - |
150 | | - if let Err(e) = ensure_yt_dlp(&target_os, &bin_dir) { |
151 | | - println!("cargo:warning=Failed to ensure yt-dlp: {e}"); |
152 | | - } |
153 | | - if let Err(e) = ensure_ffmpeg(&target_os, &bin_dir) { |
154 | | - println!("cargo:warning=Failed to ensure ffmpeg: {e}"); |
155 | | - } |
156 | | - |
157 | | - println!("cargo:rerun-if-changed={}", bin_dir.display()); |
158 | | - |
159 | | - println!("cargo:rustc-env=RESONIX_EMBED_OS_DIR={}", bin_dir.display()); |
160 | | - |
161 | | - let out_dir_fs = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set")); |
162 | | - let gen_path = out_dir_fs.join("embedded_bins.rs"); |
163 | | - let mut gen = String::new(); |
164 | | - gen.push_str("// @generated by build.rs\n"); |
165 | | - let yt = match target_os.as_str() { |
166 | | - "windows" => bin_dir.join("yt-dlp.exe"), |
167 | | - _ => bin_dir.join("yt-dlp"), |
168 | | - }; |
169 | | - if yt.exists() { |
170 | | - let rel = yt.strip_prefix(".").unwrap_or(&yt); // best-effort |
171 | | - let rel_str = rel.to_string_lossy().replace('\\', "/"); |
172 | | - gen.push_str(&format!("pub const YT_DLP_PATH: &str = r#\"{}\"#;\n", yt.display())); |
173 | | - gen.push_str(&format!( |
174 | | - "pub const YT_DLP: &[u8] = include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), r#\"/{}\"#));\n", |
175 | | - rel_str |
176 | | - )); |
177 | | - } else { |
178 | | - gen.push_str("pub const YT_DLP_PATH: &str = \"\";\npub const YT_DLP: &[u8] = &[];\n"); |
179 | | - } |
180 | | - let ff = if target_os == "windows" { bin_dir.join("ffmpeg.exe") } else { bin_dir.join("ffmpeg") }; |
181 | | - if ff.exists() { |
182 | | - let rel = ff.strip_prefix(".").unwrap_or(&ff); |
183 | | - let rel_str = rel.to_string_lossy().replace('\\', "/"); |
184 | | - gen.push_str(&format!("pub const FFMPEG_PATH: &str = r#\"{}\"#;\n", ff.display())); |
185 | | - gen.push_str(&format!( |
186 | | - "pub const FFMPEG: &[u8] = include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), r#\"/{}\"#));\n", |
187 | | - rel_str |
188 | | - )); |
189 | | - } else { |
190 | | - gen.push_str("pub const FFMPEG_PATH: &str = \"\";\npub const FFMPEG: &[u8] = &[];\n"); |
191 | | - } |
192 | 18 |
|
193 | | - // Generic embedding of every file present in assets/bin for completeness (so ffprobe, dlls, etc. are shipped). |
194 | | - let mut embedded_list_entries = String::new(); |
195 | | - embedded_list_entries |
196 | | - .push_str("pub struct EmbeddedFile { pub name: &'static str, pub bytes: &'static [u8] }\n"); |
197 | | - let mut array_items = Vec::new(); |
198 | | - if let Ok(read_dir) = fs::read_dir(&bin_dir) { |
199 | | - for entry in read_dir.flatten() { |
200 | | - if let Ok(ft) = entry.file_type() { |
201 | | - if ft.is_dir() { |
202 | | - continue; |
203 | | - } |
204 | | - } |
205 | | - let path = entry.path(); |
206 | | - if let Some(fname) = path.file_name().and_then(|s| s.to_str()) { |
207 | | - let rel = path.strip_prefix(".").unwrap_or(&path); |
208 | | - let rel_str = rel.to_string_lossy().replace('\\', "/"); |
209 | | - // Sanitize identifier |
210 | | - let mut ident = fname |
211 | | - .chars() |
212 | | - .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) |
213 | | - .collect::<String>(); |
214 | | - if !ident.chars().next().map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) { |
215 | | - ident = format!("_{}", ident); |
216 | | - } |
217 | | - ident = ident.to_ascii_uppercase(); |
218 | | - embedded_list_entries.push_str(&format!("pub const EMBED_FILE_{ident}: &[u8] = include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), r#\"/{}\"#));\n", rel_str)); |
219 | | - array_items |
220 | | - .push(format!("EmbeddedFile {{ name: r#\"{fname}\"#, bytes: EMBED_FILE_{ident} }}")); |
221 | | - } |
| 19 | + let company = env::var("RESONIX_COMPANY").unwrap_or_else(|_| "Resonix OSS Team".into()); |
| 20 | + let product = env::var("RESONIX_PRODUCT").unwrap_or_else(|_| "Resonix".into()); |
| 21 | + let copyright = env::var("RESONIX_COPYRIGHT") |
| 22 | + .unwrap_or_else(|_| "© 2025 Resonix OSS".into()); |
| 23 | + |
| 24 | + let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".into()); |
| 25 | + res.set("CompanyName", &company); |
| 26 | + res.set("FileDescription", "High-performance audio node"); |
| 27 | + res.set("ProductName", &product); |
| 28 | + res.set("ProductVersion", &version); |
| 29 | + res.set("FileVersion", &version); |
| 30 | + res.set("OriginalFilename", "resonix-node.exe"); |
| 31 | + res.set("InternalName", "resonix-node"); |
| 32 | + res.set("LegalCopyright", ©right); |
| 33 | + |
| 34 | + if let Err(e) = res.compile() { |
| 35 | + eprintln!("Failed to embed Windows resources: {e}"); |
222 | 36 | } |
223 | 37 | } |
224 | | - embedded_list_entries.push_str("pub const EMBEDDED_FILES: &[EmbeddedFile] = &[\n"); |
225 | | - for item in &array_items { |
226 | | - embedded_list_entries.push_str(" "); |
227 | | - embedded_list_entries.push_str(item); |
228 | | - embedded_list_entries.push_str(",\n"); |
229 | | - } |
230 | | - embedded_list_entries.push_str("];"); |
231 | | - gen.push_str(&embedded_list_entries); |
232 | | - if fs::write(&gen_path, gen).is_err() { |
233 | | - println!("cargo:warning=Failed to write embedded_bins.rs"); |
234 | | - } |
235 | | - println!("cargo:rustc-env=RESONIX_EMBED_BINS_RS={}", gen_path.display()); |
236 | | - println!("cargo:rustc-cfg=has_embedded_bins"); |
| 38 | + // If not Windows or no icons, nothing else to do. |
237 | 39 | } |
0 commit comments