1use crate::config::ClientConfig;
2use crate::shell::get_cache_dir;
3use anyhow::{Context, Result};
4use cloudpub_common::downloads::{release_file_name, ArtifactKind};
5use cloudpub_common::protocol::message::Message;
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11
12#[cfg(unix)]
14const UNIX_SERVICE_UPDATE_SCRIPT: &str = include_str!("scripts/unix_service_update.sh");
15#[cfg(unix)]
16const UNIX_CLIENT_UPDATE_SCRIPT: &str = include_str!("scripts/unix_client_update.sh");
17#[cfg(windows)]
18const WINDOWS_SERVICE_UPDATE_SCRIPT: &str = include_str!("scripts/windows_service_update.bat");
19#[cfg(windows)]
20const WINDOWS_CLIENT_UPDATE_SCRIPT: &str = include_str!("scripts/windows_client_update.bat");
21#[cfg(windows)]
22const WINDOWS_MSI_INSTALL_SCRIPT: &str = include_str!("scripts/windows_msi_install.bat");
23#[cfg(target_os = "macos")]
24const MACOS_DMG_INSTALL_SCRIPT: &str = include_str!("scripts/macos_dmg_install.sh");
25#[cfg(target_os = "linux")]
26const LINUX_DEB_INSTALL_SCRIPT: &str = include_str!("scripts/linux_deb_install.sh");
27#[cfg(target_os = "linux")]
28const LINUX_RPM_INSTALL_SCRIPT: &str = include_str!("scripts/linux_rpm_install.sh");
29#[cfg(target_os = "linux")]
30const SYSTEMD_ONESHOT_TEMPLATE: &str = include_str!("scripts/systemd_oneshot.service");
31#[cfg(target_os = "macos")]
32const LAUNCHD_ONESHOT_TEMPLATE: &str = include_str!("scripts/launchd_oneshot.plist");
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PlatformDownloads {
36 pub gui: Option<String>,
37 pub cli: Option<String>,
38 pub rpm: Option<String>,
39 pub deb: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct OsDownloads {
44 pub name: String,
45 pub platforms: HashMap<String, PlatformDownloads>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct DownloadConfig {
50 pub macos: OsDownloads,
51 pub windows: OsDownloads,
52 pub linux: OsDownloads,
53 pub android: OsDownloads,
54}
55
56impl DownloadConfig {
57 pub fn new(base_url: &str, version: &str, branch: &str) -> Self {
58 fn os_downloads(
66 base_url: &str,
67 version: &str,
68 branch: &str,
69 name: &str,
70 os: &str,
71 arches: &[&str],
72 ) -> OsDownloads {
73 let mut platforms = HashMap::new();
74 for arch in arches {
75 let of_kind = |kind: ArtifactKind| {
76 release_file_name(version, branch, os, arch, kind)
77 .map(|file| format!("{base_url}{file}"))
78 };
79 platforms.insert(
80 arch.to_string(),
81 PlatformDownloads {
82 gui: of_kind(ArtifactKind::Gui),
83 cli: of_kind(ArtifactKind::Cli),
84 rpm: of_kind(ArtifactKind::Rpm),
85 deb: of_kind(ArtifactKind::Deb),
86 },
87 );
88 }
89 OsDownloads {
90 name: name.to_string(),
91 platforms,
92 }
93 }
94
95 Self {
96 macos: os_downloads(
97 base_url,
98 version,
99 branch,
100 "macOS",
101 "macos",
102 &["aarch64", "x86_64"],
103 ),
104 windows: os_downloads(base_url, version, branch, "Windows", "windows", &["x86_64"]),
105 linux: os_downloads(
106 base_url,
107 version,
108 branch,
109 "Linux",
110 "linux",
111 &[
112 "arm", "armv5te", "aarch64", "mips", "mipsel", "i686", "x86_64",
113 ],
114 ),
115 android: os_downloads(
116 base_url,
117 version,
118 branch,
119 "Android",
120 "android",
121 &["aarch64"],
122 ),
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum DownloadType {
129 Gui,
130 Cli,
131 Rpm,
132 Deb,
133}
134
135pub fn get_current_os() -> String {
136 #[cfg(target_os = "macos")]
137 return "macos".to_string();
138 #[cfg(target_os = "windows")]
139 return "windows".to_string();
140 #[cfg(target_os = "linux")]
141 return "linux".to_string();
142 #[cfg(target_os = "android")]
143 return "android".to_string();
144 #[cfg(not(any(
145 target_os = "macos",
146 target_os = "windows",
147 target_os = "linux",
148 target_os = "android"
149 )))]
150 return std::env::consts::OS.to_string();
151}
152
153pub fn get_current_arch() -> String {
154 #[cfg(target_arch = "x86_64")]
155 return "x86_64".to_string();
156 #[cfg(target_arch = "aarch64")]
157 return "aarch64".to_string();
158 #[cfg(target_arch = "arm")]
159 return "arm".to_string();
160 #[cfg(target_arch = "x86")]
161 return "i686".to_string();
162 #[cfg(not(any(
163 target_arch = "x86_64",
164 target_arch = "aarch64",
165 target_arch = "arm",
166 target_arch = "x86"
167 )))]
168 return std::env::consts::ARCH.to_string();
169}
170
171#[cfg(target_os = "linux")]
172pub fn get_linux_package_type() -> Result<String> {
173 if std::process::Command::new("which")
175 .arg("dpkg")
176 .output()
177 .map(|output| output.status.success())
178 .unwrap_or(false)
179 {
180 return Ok("deb".to_string());
181 }
182
183 if std::process::Command::new("which")
185 .arg("rpm")
186 .output()
187 .map(|output| output.status.success())
188 .unwrap_or(false)
189 {
190 return Ok("rpm".to_string());
191 }
192
193 Err(anyhow::anyhow!("Could not detect package type"))
194}
195
196pub fn get_download_url(
197 base_url: &str,
198 download_type: &DownloadType,
199 version: &str,
200 branch: &str,
201) -> Result<String> {
202 let base_url = format!("{}download/{}/", base_url, branch);
203 let config = DownloadConfig::new(&base_url, version, branch);
204 let os = get_current_os();
205 let arch = get_current_arch();
206
207 let os_downloads = match os.as_str() {
208 "macos" => &config.macos,
209 "windows" => &config.windows,
210 "linux" => &config.linux,
211 "android" => &config.android,
212 _ => {
213 return Err(anyhow::anyhow!("Unsupported OS: {}", os))
214 .context("Failed to get OS downloads")
215 }
216 };
217
218 let platform_downloads = os_downloads
219 .platforms
220 .get(&arch)
221 .with_context(|| format!("Unsupported architecture {} for OS {}", arch, os))?;
222
223 let download_url = match download_type {
224 DownloadType::Gui => platform_downloads
225 .gui
226 .as_ref()
227 .context("GUI download not available for this platform")?,
228 DownloadType::Cli => platform_downloads
229 .cli
230 .as_ref()
231 .context("CLI download not available for this platform")?,
232 DownloadType::Rpm => platform_downloads
233 .rpm
234 .as_ref()
235 .context("RPM download not available for this platform")?,
236 DownloadType::Deb => platform_downloads
237 .deb
238 .as_ref()
239 .context("DEB download not available for this platform")?,
240 };
241
242 Ok(download_url.clone())
243}
244
245#[allow(unused_variables)]
247fn run_script(
248 script_name: &str,
249 script_template: &str,
250 replacements: Vec<(&str, String)>,
251 is_service: bool,
252) -> Result<()> {
253 use anyhow::Context;
254 use tracing::{debug, info};
255
256 let cache_dir = get_cache_dir("updates")?;
258 std::fs::create_dir_all(&cache_dir).context("Failed to create updates cache directory")?;
259
260 let timestamp = format!(
261 "{}_{}",
262 std::process::id(),
263 std::time::SystemTime::now()
264 .duration_since(std::time::UNIX_EPOCH)
265 .unwrap_or_default()
266 .as_secs()
267 );
268
269 #[cfg(windows)]
270 let script_path = cache_dir.join(format!("{}_{}.bat", script_name, timestamp));
271 #[cfg(unix)]
272 let script_path = cache_dir.join(format!("{}_{}.sh", script_name, timestamp));
273
274 debug!("Creating script: {}", script_path.display());
275
276 let mut script_content = script_template.to_string();
278 for (placeholder, value) in replacements {
279 script_content = script_content.replace(placeholder, &value);
280 }
281
282 std::fs::write(&script_path, script_content).context("Failed to create script")?;
283
284 info!("Script created at: {}", script_path.display());
285
286 #[cfg(unix)]
288 {
289 use std::os::unix::fs::PermissionsExt;
290 let mut perms = std::fs::metadata(&script_path)?.permissions();
291 perms.set_mode(0o755);
292 std::fs::set_permissions(&script_path, perms)?;
293 }
294
295 #[cfg(windows)]
297 {
298 use std::os::windows::process::CommandExt;
299 std::process::Command::new("cmd")
300 .args(&["/C", &script_path.to_string_lossy()])
301 .creation_flags(0x08000000) .spawn()
303 .context("Failed to execute script")?;
304 }
305
306 #[cfg(target_os = "linux")]
307 {
308 if is_service {
309 let service_name = format!("cloudpub-upgrade-{}.service", timestamp);
311 let service_path = format!("/etc/systemd/system/{}", service_name);
312
313 debug!("Creating systemd one-shot service: {}", service_name);
314
315 let service_content = SYSTEMD_ONESHOT_TEMPLATE
316 .replace("{TIMESTAMP}", ×tamp)
317 .replace("{SCRIPT_PATH}", &script_path.display().to_string());
318
319 std::fs::write(&service_path, service_content)
320 .context("Failed to create systemd one-shot service")?;
321
322 std::process::Command::new("systemctl")
323 .arg("daemon-reload")
324 .output()
325 .context("Failed to reload systemd")?;
326
327 std::process::Command::new("systemctl")
328 .arg("start")
329 .arg(&service_name)
330 .spawn()
331 .context("Failed to start upgrade service")?;
332
333 info!(
334 "Upgrade scheduled via systemd one-shot service: {}",
335 service_name
336 );
337 } else {
338 debug!("Executing script with nohup: {}", script_path.display());
340
341 std::process::Command::new("nohup")
342 .arg("/bin/bash")
343 .arg(&script_path)
344 .stdin(std::process::Stdio::null())
345 .stdout(std::process::Stdio::null())
346 .stderr(std::process::Stdio::null())
347 .spawn()
348 .context("Failed to spawn script with nohup")?;
349
350 info!("Script spawned with nohup");
351 }
352 }
353
354 #[cfg(target_os = "macos")]
355 {
356 if is_service {
357 let plist_name = format!("com.cloudpub.upgrade.{}.plist", timestamp);
359 let plist_path = format!("/Library/LaunchDaemons/{}", plist_name);
360
361 debug!("Creating launchd one-shot job: {}", plist_name);
362
363 let plist_content = LAUNCHD_ONESHOT_TEMPLATE
364 .replace("{TIMESTAMP}", ×tamp)
365 .replace("{SCRIPT_PATH}", &script_path.display().to_string());
366
367 std::fs::write(&plist_path, plist_content)
368 .context("Failed to create launchd one-shot plist")?;
369
370 std::process::Command::new("chmod")
371 .args(["644", &plist_path])
372 .output()
373 .context("Failed to set plist permissions")?;
374
375 std::process::Command::new("launchctl")
376 .args(["load", &plist_path])
377 .spawn()
378 .context("Failed to load upgrade job")?;
379
380 info!("Upgrade scheduled via launchd one-shot job: {}", plist_name);
381 } else {
382 debug!("Executing script with nohup: {}", script_path.display());
384
385 std::process::Command::new("nohup")
386 .arg("/bin/bash")
387 .arg(&script_path)
388 .stdin(std::process::Stdio::null())
389 .stdout(std::process::Stdio::null())
390 .stderr(std::process::Stdio::null())
391 .spawn()
392 .context("Failed to spawn script with nohup")?;
393
394 info!("Script spawned with nohup");
395 }
396 }
397
398 info!("Script execution initiated, exiting current process");
399 std::process::exit(0);
400}
401
402fn run_upgrade_script(
404 script_template: &str,
405 new_exe_path: &std::path::Path,
406 current_args: Vec<String>,
407 is_service: bool,
408) -> Result<()> {
409 use tracing::debug;
410
411 let current_exe = std::env::current_exe()?;
412 let cache_dir = get_cache_dir("updates")?;
413 std::fs::create_dir_all(&cache_dir).context("Failed to create updates cache directory")?;
414
415 let timestamp = format!(
416 "{}_{}",
417 std::process::id(),
418 std::time::SystemTime::now()
419 .duration_since(std::time::UNIX_EPOCH)
420 .unwrap_or_default()
421 .as_secs()
422 );
423
424 let log_file = cache_dir.join(format!("cloudpub_update_{}.log", timestamp));
425
426 debug!("Starting update process");
427 debug!("Current executable: {}", current_exe.display());
428 debug!("New executable: {}", new_exe_path.display());
429 debug!("Current args: {:?}", current_args);
430 debug!("Log file location: {}", log_file.display());
431 debug!("Is service: {}", is_service);
432
433 let config_path = if is_service {
436 current_args
437 .windows(2)
438 .find(|w| w[0] == "--conf" || w[0] == "--config")
439 .and_then(|w| w.get(1))
440 .map(|s| s.as_str())
441 .unwrap_or("")
442 .to_string()
443 } else {
444 String::new()
445 };
446
447 let replacements = vec![
449 ("{LOG_FILE}", log_file.display().to_string()),
450 ("{NEW_EXE}", new_exe_path.display().to_string()),
451 ("{CURRENT_EXE}", current_exe.display().to_string()),
452 ("{ARGS}", current_args.join(" ")),
453 ("{CONFIG_PATH}", config_path),
454 ("{SERVICE_ARGS}", current_args.join(" ")),
455 ];
456
457 run_script("cloudpub_update", script_template, replacements, is_service)
458}
459
460#[cfg(windows)]
461pub fn install_msi_package(msi_path: &std::path::Path) -> Result<()> {
462 let current_exe = std::env::current_exe()?;
463
464 run_script(
465 "cloudpub_install_msi",
466 WINDOWS_MSI_INSTALL_SCRIPT,
467 vec![
468 ("{MSI_PATH}", msi_path.display().to_string()),
469 ("{CURRENT_EXE}", current_exe.display().to_string()),
470 ],
471 false,
472 )
473}
474
475#[cfg(windows)]
476pub fn replace_and_restart_windows(
477 new_exe_path: &std::path::Path,
478 current_args: Vec<String>,
479) -> Result<()> {
480 let is_service = current_args.contains(&"--run-as-service".to_string());
481 let script_template = if is_service {
482 WINDOWS_SERVICE_UPDATE_SCRIPT
483 } else {
484 WINDOWS_CLIENT_UPDATE_SCRIPT
485 };
486 run_upgrade_script(script_template, new_exe_path, current_args, is_service)
487}
488
489#[cfg(not(windows))]
490pub fn replace_and_restart_unix(
491 new_exe_path: &std::path::Path,
492 current_args: Vec<String>,
493) -> Result<()> {
494 let is_service = current_args.contains(&"--run-as-service".to_string());
495 let script_template = if is_service {
496 UNIX_SERVICE_UPDATE_SCRIPT
497 } else {
498 UNIX_CLIENT_UPDATE_SCRIPT
499 };
500 run_upgrade_script(script_template, new_exe_path, current_args, is_service)
501}
502
503pub fn apply_update_and_restart(new_exe_path: &std::path::Path) -> Result<()> {
504 let args: Vec<String> = std::env::args().skip(1).collect();
506
507 eprintln!("{}", crate::t!("applying-update"));
508
509 #[cfg(windows)]
510 replace_and_restart_windows(new_exe_path, args)?;
511
512 #[cfg(not(windows))]
513 replace_and_restart_unix(new_exe_path, args)?;
514
515 Ok(())
516}
517
518#[cfg(target_os = "macos")]
519async fn install_dmg_package(package_path: &std::path::Path) -> Result<()> {
520 eprintln!("Installing DMG package...");
521
522 let current_exe = std::env::current_exe()?;
523 let current_args: Vec<String> = std::env::args().skip(1).collect();
524
525 run_script(
526 "cloudpub_install_dmg",
527 MACOS_DMG_INSTALL_SCRIPT,
528 vec![
529 ("{PACKAGE_PATH}", package_path.display().to_string()),
530 ("{CURRENT_EXE}", current_exe.display().to_string()),
531 ("{CURRENT_ARGS}", current_args.join(" ")),
532 ],
533 false,
534 )
535}
536
537#[cfg(target_os = "linux")]
538async fn install_deb_package(package_path: &std::path::Path) -> Result<()> {
539 eprintln!("Installing DEB package...");
540
541 let current_exe = std::env::current_exe()?;
542 let current_args: Vec<String> = std::env::args().skip(1).collect();
543
544 run_script(
545 "cloudpub_install_deb",
546 LINUX_DEB_INSTALL_SCRIPT,
547 vec![
548 ("{PACKAGE_PATH}", package_path.display().to_string()),
549 ("{CURRENT_EXE}", current_exe.display().to_string()),
550 ("{CURRENT_ARGS}", current_args.join(" ")),
551 ],
552 false,
553 )
554}
555
556#[cfg(target_os = "linux")]
557async fn install_rpm_package(package_path: &std::path::Path) -> Result<()> {
558 eprintln!("Installing RPM package...");
559
560 let current_exe = std::env::current_exe()?;
561 let current_args: Vec<String> = std::env::args().skip(1).collect();
562
563 run_script(
564 "cloudpub_install_rpm",
565 LINUX_RPM_INSTALL_SCRIPT,
566 vec![
567 ("{PACKAGE_PATH}", package_path.display().to_string()),
568 ("{CURRENT_EXE}", current_exe.display().to_string()),
569 ("{CURRENT_ARGS}", current_args.join(" ")),
570 ],
571 false,
572 )
573}
574
575async fn handle_cli_update(
576 filename: &str,
577 output_path: &std::path::Path,
578 cache_dir: &std::path::Path,
579 command_rx: &mut mpsc::Receiver<Message>,
580 result_tx: &mpsc::Sender<Message>,
581) -> Result<()> {
582 let unpack_message = "Unpacking update".to_string();
583
584 if filename.ends_with(".zip") {
585 crate::shell::unzip(&unpack_message, output_path, cache_dir, 0, result_tx)
586 .await
587 .context("Failed to unpack zip update")?;
588 } else if filename.ends_with(".tar.gz") {
589 let tar_args = vec![
590 "-xzf".to_string(),
591 output_path.to_string_lossy().to_string(),
592 ];
593
594 let total_files = 1;
595 let progress = Some((unpack_message, result_tx.clone(), total_files));
596
597 crate::shell::execute(
598 std::path::PathBuf::from("tar"),
599 tar_args,
600 Some(cache_dir.to_path_buf()),
601 std::collections::HashMap::new(),
602 progress,
603 command_rx,
604 )
605 .await
606 .context("Failed to unpack tar.gz update")?;
607 } else {
608 return Err(anyhow::anyhow!("Unknown file format: {}", filename));
609 }
610
611 eprintln!(
612 "{}",
613 crate::t!("update-unpacked", "path" => cache_dir.display().to_string())
614 );
615
616 #[cfg(not(windows))]
617 let new_exe_path = cache_dir.join("clo");
618 #[cfg(windows)]
619 let new_exe_path = cache_dir.join("clo.exe");
620
621 apply_update_and_restart(&new_exe_path)?;
622 Ok(())
623}
624
625pub async fn handle_upgrade_download(
626 version: &str,
627 gui: bool,
628 config: Arc<RwLock<ClientConfig>>,
629 command_rx: &mut mpsc::Receiver<Message>,
630 result_tx: &mpsc::Sender<Message>,
631) -> Result<(std::path::PathBuf, DownloadType, String)> {
632 let cache_dir = get_cache_dir("updates").unwrap();
634 std::fs::remove_dir_all(&cache_dir).ok();
635
636 let cache_dir = get_cache_dir("updates").unwrap();
638
639 let download_type = if gui {
640 #[cfg(target_os = "linux")]
641 {
642 match get_linux_package_type() {
644 Ok(pkg_type) if pkg_type == "deb" => DownloadType::Deb,
645 Ok(pkg_type) if pkg_type == "rpm" => DownloadType::Rpm,
646 _ => DownloadType::Cli, }
648 }
649 #[cfg(not(target_os = "linux"))]
650 DownloadType::Gui
651 } else {
652 DownloadType::Cli
653 };
654
655 let base_url = config.read().server.to_string();
657 let download_url =
658 get_download_url(&base_url, &download_type, version, cloudpub_common::BRANCH)?;
659
660 let mut file_extension = std::path::Path::new(&download_url)
662 .extension()
663 .and_then(|ext| ext.to_str())
664 .map(|ext| format!(".{}", ext))
665 .unwrap_or_else(|| ".tar.gz".to_string());
666
667 if file_extension == ".gz" {
668 file_extension = ".tar.gz".to_string();
670 }
671
672 let filename = format!("cloudpub-{}{}", version, file_extension);
673 let output_path = cache_dir.join(&filename);
674
675 let message = crate::t!("downloading-update", "path" => output_path.display().to_string());
676
677 crate::shell::download(
678 &message,
679 config.clone(),
680 &download_url,
681 &output_path,
682 command_rx,
683 result_tx,
684 )
685 .await
686 .with_context(|| format!("Failed to download update ({})", download_url))?;
687
688 eprintln!(
689 "{}",
690 crate::t!("update-downloaded", "path" => output_path.display().to_string())
691 );
692
693 Ok((output_path, download_type, filename))
694}
695
696pub async fn handle_upgrade_install(
697 output_path: std::path::PathBuf,
698 download_type: DownloadType,
699 filename: String,
700 command_rx: &mut mpsc::Receiver<Message>,
701 result_tx: &mpsc::Sender<Message>,
702) -> Result<()> {
703 let cache_dir = output_path.parent().unwrap();
704 std::env::set_current_dir(cache_dir)
706 .context("Failed to change current directory to cache directory")?;
707
708 match download_type {
710 DownloadType::Gui => {
711 #[cfg(target_os = "macos")]
712 {
713 if filename.ends_with(".dmg") {
714 install_dmg_package(&output_path).await?;
715 } else {
716 return Err(anyhow::anyhow!("Unsupported GUI package format"));
717 }
718 }
719 #[cfg(target_os = "windows")]
720 {
721 if filename.ends_with(".msi") {
722 install_msi_package(&output_path)?;
723 } else {
724 return Err(anyhow::anyhow!("Unsupported GUI package format"));
725 }
726 }
727 #[cfg(target_os = "linux")]
728 {
729 return Err(anyhow::anyhow!("GUI packages not supported on Linux"));
730 }
731 }
732 DownloadType::Deb => {
733 #[cfg(target_os = "linux")]
734 {
735 install_deb_package(&output_path).await?;
736 }
737 #[cfg(not(target_os = "linux"))]
738 {
739 return Err(anyhow::anyhow!("DEB packages only supported on Linux"));
740 }
741 }
742 DownloadType::Rpm => {
743 #[cfg(target_os = "linux")]
744 {
745 install_rpm_package(&output_path).await?;
746 }
747 #[cfg(not(target_os = "linux"))]
748 {
749 return Err(anyhow::anyhow!("RPM packages only supported on Linux"));
750 }
751 }
752 _ => {
753 handle_cli_update(&filename, &output_path, cache_dir, command_rx, result_tx).await?;
755 }
756 }
757
758 Ok(())
759}
760
761pub async fn handle_upgrade_available(
762 version: &str,
763 config: Arc<RwLock<ClientConfig>>,
764 gui: bool,
765 command_rx: &mut mpsc::Receiver<Message>,
766 result_tx: &mpsc::Sender<Message>,
767) -> Result<()> {
768 let (output_path, download_type, filename) =
769 handle_upgrade_download(version, gui, config, command_rx, result_tx).await?;
770
771 handle_upgrade_install(output_path, download_type, filename, command_rx, result_tx).await?;
772
773 Ok(())
774}
775
776#[cfg(test)]
777mod download_config_tests {
778 use super::*;
779
780 #[test]
790 fn download_urls_are_unchanged_for_every_platform() {
791 let config = DownloadConfig::new("https://cloudpub.ru/download/beta/", "3.5.1126", "beta");
792
793 assert_eq!(
794 config.linux.platforms["x86_64"].cli.as_deref(),
795 Some("https://cloudpub.ru/download/beta/clo-3.5.1126-beta-linux-x86_64.tar.gz")
796 );
797 assert_eq!(
798 config.linux.platforms["x86_64"].deb.as_deref(),
799 Some("https://cloudpub.ru/download/beta/cloudpub-3.5.1126-beta-linux-x86_64.deb")
800 );
801 assert_eq!(
802 config.linux.platforms["arm"].cli.as_deref(),
803 Some("https://cloudpub.ru/download/beta/clo-3.5.1126-beta-linux-arm.tar.gz")
804 );
805 assert_eq!(config.linux.platforms["x86_64"].gui, None);
806 assert_eq!(
807 config.macos.platforms["aarch64"].gui.as_deref(),
808 Some("https://cloudpub.ru/download/beta/cloudpub-3.5.1126-beta-macos-aarch64.dmg")
809 );
810 assert_eq!(
811 config.windows.platforms["x86_64"].cli.as_deref(),
812 Some("https://cloudpub.ru/download/beta/clo-3.5.1126-beta-windows-x86_64.zip")
813 );
814 assert_eq!(
815 config.windows.platforms["x86_64"].gui.as_deref(),
816 Some("https://cloudpub.ru/download/beta/cloudpub-3.5.1126-beta-windows-x86_64.msi")
817 );
818 assert_eq!(
819 config.android.platforms["aarch64"].cli.as_deref(),
820 Some("https://cloudpub.ru/download/beta/clo-3.5.1126-beta-android-arm64.tar.gz")
821 );
822 }
823}