1use sha2::{Digest, Sha256};
4use std::env::consts::{ARCH, OS};
5use std::path::PathBuf;
6
7const LIGHTPANDA_BASE_URL: &str =
8 "https://github.com/lightpanda-io/browser/releases/download/nightly";
9
10pub async fn run_setup() {
12 println!();
13 let (os_label, arch_label, binary_name) = match (OS, ARCH) {
14 ("linux", "x86_64") => ("Linux", "x86_64", "lightpanda-x86_64-linux"),
15 ("macos", "aarch64") => ("macOS", "aarch64", "lightpanda-aarch64-macos"),
16 _ => {
17 eprintln!(" ✗ Unsupported platform: {OS} {ARCH}");
18 eprintln!(" LightPanda provides binaries for Linux x86_64 and macOS aarch64.");
19 std::process::exit(1);
20 }
21 };
22
23 println!(" → Detected: {os_label} {arch_label}");
24
25 let install_dir = home_local_bin();
27 let install_path = install_dir.join("lightpanda");
28
29 println!(" → Downloading LightPanda...");
30
31 let url = format!("{LIGHTPANDA_BASE_URL}/{binary_name}");
32 let bytes = match download_binary(&url).await {
33 Ok(b) => b,
34 Err(e) => {
35 eprintln!(" ✗ Download failed: {e}");
36 std::process::exit(1);
37 }
38 };
39
40 let actual_hash = sha256_hex(&bytes);
42 let checksum_url = format!("{LIGHTPANDA_BASE_URL}/{binary_name}.sha256");
43 match download_checksum(&checksum_url).await {
44 Ok(expected_hash) => {
45 if actual_hash != expected_hash {
46 eprintln!(" ✗ SHA256 checksum mismatch!");
47 eprintln!(" Expected: {expected_hash}");
48 eprintln!(" Actual: {actual_hash}");
49 eprintln!(" The downloaded binary may be corrupted or tampered with.");
50 std::process::exit(1);
51 }
52 println!(" ✓ SHA256 checksum verified: {actual_hash}");
53 }
54 Err(_) => {
55 println!(" ⚠ No checksum file available, SHA256: {actual_hash}");
57 }
58 }
59
60 if let Err(e) = std::fs::create_dir_all(&install_dir) {
61 eprintln!(" ✗ Failed to create {}: {e}", install_dir.display());
62 std::process::exit(1);
63 }
64
65 if let Err(e) = std::fs::write(&install_path, &bytes) {
66 eprintln!(" ✗ Failed to write {}: {e}", install_path.display());
67 std::process::exit(1);
68 }
69
70 #[cfg(unix)]
71 {
72 use std::os::unix::fs::PermissionsExt;
73 let perms = std::fs::Permissions::from_mode(0o755);
74 if let Err(e) = std::fs::set_permissions(&install_path, perms) {
75 eprintln!(" ✗ Failed to chmod +x: {e}");
76 std::process::exit(1);
77 }
78 }
79
80 println!(" ✓ Installed to {}", install_path.display());
81
82 let config_path = PathBuf::from("config.local.toml");
84 if config_path.exists() {
85 println!(" ✓ config.local.toml already exists (skipped)");
86 } else {
87 let config_content = r#"[renderer]
88mode = "lightpanda"
89
90[renderer.lightpanda]
91ws_url = "ws://127.0.0.1:9222/"
92"#;
93 if let Err(e) = std::fs::write(&config_path, config_content) {
94 eprintln!(" ✗ Failed to write config.local.toml: {e}");
95 std::process::exit(1);
96 }
97 println!(" ✓ Created config.local.toml");
98 }
99
100 println!();
101 println!(" Start the server with JS rendering:");
102 println!(" lightpanda serve --host 127.0.0.1 --port 9222 &");
103 println!(" crw-server");
104 println!();
105}
106
107async fn download_binary(url: &str) -> Result<Vec<u8>, reqwest::Error> {
108 let client = reqwest::Client::builder()
109 .redirect(crw_core::url_safety::safe_redirect_policy())
110 .build()?;
111
112 let resp = client.get(url).send().await?.error_for_status()?;
113 let bytes = resp.bytes().await?;
114 Ok(bytes.to_vec())
115}
116
117async fn download_checksum(url: &str) -> Result<String, String> {
119 let client = reqwest::Client::builder()
120 .redirect(crw_core::url_safety::safe_redirect_policy())
121 .build()
122 .map_err(|e| format!("client build error: {e}"))?;
123
124 let resp = client
125 .get(url)
126 .send()
127 .await
128 .map_err(|e| format!("fetch error: {e}"))?;
129
130 if !resp.status().is_success() {
131 return Err(format!("HTTP {}", resp.status()));
132 }
133
134 let text = resp.text().await.map_err(|e| format!("read error: {e}"))?;
135
136 let hash = text
138 .split_whitespace()
139 .next()
140 .ok_or_else(|| "empty checksum file".to_string())?
141 .to_lowercase();
142
143 if hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
145 return Err(format!("invalid checksum format: {hash}"));
146 }
147
148 Ok(hash)
149}
150
151fn sha256_hex(data: &[u8]) -> String {
152 let mut hasher = Sha256::new();
153 hasher.update(data);
154 hex::encode(hasher.finalize())
155}
156
157fn home_local_bin() -> PathBuf {
158 let home = std::env::var("HOME")
159 .or_else(|_| std::env::var("USERPROFILE"))
160 .unwrap_or_else(|_| ".".to_string());
161 PathBuf::from(home).join(".local").join("bin")
162}