|
1 | | -use std::fs; |
2 | | -use std::io::{Read as _, Write as _}; |
3 | | -use std::path::PathBuf; |
| 1 | +use std::io::{self, IsTerminal as _, Write as _}; |
| 2 | +use std::path::{Path, PathBuf}; |
| 3 | +use std::process::Command; |
4 | 4 |
|
5 | 5 | use anyhow::{bail, Context as _, Result}; |
6 | 6 | use log::info; |
7 | | -use sha2::{Digest as _, Sha256}; |
8 | 7 |
|
9 | | -use crate::config::Config; |
10 | | - |
11 | | -const ODIFF_VERSION: &str = "4.3.8"; |
12 | | -const ODIFF_TARBALL_SHA256: &str = |
13 | | - "cf5a344223fdbbdd6367a08a412410a0c9e98fea65c9b14efbc745c42f054fb3"; |
14 | | - |
15 | | -fn odiff_tarball_url() -> String { |
16 | | - format!("https://registry.npmjs.org/odiff-bin/-/odiff-bin-{ODIFF_VERSION}.tgz") |
17 | | -} |
18 | | - |
19 | | -fn platform_binary_name() -> Result<&'static str> { |
20 | | - match (std::env::consts::OS, std::env::consts::ARCH) { |
21 | | - ("macos", "aarch64") => Ok("odiff-macos-arm64"), |
22 | | - ("macos", "x86_64") => Ok("odiff-macos-x64"), |
23 | | - ("linux", "x86_64") => Ok("odiff-linux-x64"), |
24 | | - ("linux", "aarch64") => Ok("odiff-linux-arm64"), |
25 | | - ("windows", "x86_64") => Ok("odiff-win-x64.exe"), |
26 | | - (os, arch) => bail!("Unsupported platform for odiff: {os}/{arch}"), |
27 | | - } |
28 | | -} |
29 | | - |
30 | | -fn cache_dir() -> Result<PathBuf> { |
31 | | - let home = dirs::home_dir().context("Could not determine home directory")?; |
32 | | - Ok(home.join(".sentry-cli").join("odiff").join(ODIFF_VERSION)) |
33 | | -} |
34 | | - |
35 | | -fn download_and_extract() -> Result<PathBuf> { |
36 | | - let binary_name = platform_binary_name()?; |
37 | | - let cache = cache_dir()?; |
38 | | - let binary_path = cache.join(binary_name); |
39 | | - |
40 | | - fs::create_dir_all(&cache).context("Failed to create odiff cache directory")?; |
41 | | - |
42 | | - info!("Downloading odiff {ODIFF_VERSION} from npm registry..."); |
43 | | - |
44 | | - let mut data = Vec::new(); |
45 | | - let mut handle = curl::easy::Easy::new(); |
46 | | - handle.url(&odiff_tarball_url())?; |
47 | | - handle.follow_location(true)?; |
48 | | - |
49 | | - if let Some(config) = Config::current_opt() { |
50 | | - if let Some(proxy_url) = config.get_proxy_url() { |
51 | | - handle.proxy(&proxy_url)?; |
52 | | - } |
53 | | - if let Some(proxy_username) = config.get_proxy_username() { |
54 | | - handle.proxy_username(proxy_username)?; |
55 | | - } |
56 | | - if let Some(proxy_password) = config.get_proxy_password() { |
57 | | - handle.proxy_password(proxy_password)?; |
| 8 | +const MIN_SUPPORTED_VERSION: &str = "4.0.0"; |
| 9 | + |
| 10 | +fn check_version(binary_path: &Path) { |
| 11 | + let output = Command::new(binary_path).arg("--version").output(); |
| 12 | + |
| 13 | + match output { |
| 14 | + Ok(out) if out.status.success() => { |
| 15 | + let version_str = String::from_utf8_lossy(&out.stdout).trim().to_owned(); |
| 16 | + let version = version_str |
| 17 | + .strip_prefix("odiff ") |
| 18 | + .unwrap_or(&version_str) |
| 19 | + .trim(); |
| 20 | + |
| 21 | + let min = semver::Version::parse(MIN_SUPPORTED_VERSION) |
| 22 | + .expect("MIN_SUPPORTED_VERSION is valid semver"); |
| 23 | + if let Ok(installed) = semver::Version::parse(version) { |
| 24 | + if installed < min { |
| 25 | + eprintln!( |
| 26 | + "Warning: odiff {version} is below the minimum supported version \ |
| 27 | + ({MIN_SUPPORTED_VERSION}). You may experience issues." |
| 28 | + ); |
| 29 | + } |
| 30 | + } |
58 | 31 | } |
59 | | - handle.ssl_verify_host(config.should_verify_ssl())?; |
60 | | - handle.ssl_verify_peer(config.should_verify_ssl())?; |
61 | | - let mut ssl_opts = curl::easy::SslOpt::new(); |
62 | | - if config.disable_ssl_revocation_check() { |
63 | | - ssl_opts.no_revoke(true); |
| 32 | + _ => { |
| 33 | + eprintln!("Warning: Could not determine odiff version. You may experience issues."); |
64 | 34 | } |
65 | | - handle.ssl_options(&ssl_opts)?; |
66 | 35 | } |
| 36 | +} |
67 | 37 |
|
68 | | - if let Ok(ca_bundle) = std::env::var("CURL_CA_BUNDLE") { |
69 | | - handle.cainfo(&ca_bundle)?; |
70 | | - } else if let Ok(ca_bundle) = std::env::var("SSL_CERT_FILE") { |
71 | | - handle.cainfo(&ca_bundle)?; |
72 | | - } |
73 | | - { |
74 | | - let mut transfer = handle.transfer(); |
75 | | - transfer.write_function(|chunk| { |
76 | | - data.extend_from_slice(chunk); |
77 | | - Ok(chunk.len()) |
78 | | - })?; |
79 | | - transfer |
80 | | - .perform() |
81 | | - .context("Failed to download odiff tarball")?; |
| 38 | +fn prompt_npm_install() -> Result<PathBuf> { |
| 39 | + if !io::stdin().is_terminal() || !io::stderr().is_terminal() { |
| 40 | + bail!( |
| 41 | + "This command requires `odiff`, but it is not installed.\n\n\ |
| 42 | + `odiff` can be installed with npm:\n\n \ |
| 43 | + npm install -g odiff-bin" |
| 44 | + ); |
82 | 45 | } |
83 | 46 |
|
84 | | - let status = handle.response_code()?; |
85 | | - if status != 200 { |
86 | | - bail!("Failed to download odiff: HTTP {status}"); |
87 | | - } |
| 47 | + eprintln!("This command requires `odiff`, but it is not installed."); |
| 48 | + eprintln!(); |
| 49 | + eprintln!("`odiff` can be installed with npm:"); |
| 50 | + eprintln!(); |
| 51 | + eprintln!(" npm install -g odiff-bin"); |
| 52 | + eprintln!(); |
| 53 | + eprint!("Would you like us to install `odiff` with npm for you? [y/N] "); |
| 54 | + io::stderr().flush()?; |
| 55 | + |
| 56 | + let mut input = String::new(); |
| 57 | + io::stdin().read_line(&mut input)?; |
88 | 58 |
|
89 | | - let digest = Sha256::digest(&data); |
90 | | - let hash = digest.iter().fold(String::new(), |mut acc, b| { |
91 | | - use std::fmt::Write as _; |
92 | | - write!(acc, "{b:02x}").expect("writing hex to String should never fail"); |
93 | | - acc |
94 | | - }); |
95 | | - if hash != ODIFF_TARBALL_SHA256 { |
| 59 | + if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") { |
96 | 60 | bail!( |
97 | | - "odiff tarball integrity check failed: expected SHA-256 {ODIFF_TARBALL_SHA256}, got {hash}" |
| 61 | + "`odiff` is required but not installed. \ |
| 62 | + Install it with: npm install -g odiff-bin" |
98 | 63 | ); |
99 | 64 | } |
100 | 65 |
|
101 | | - let decoder = flate2::read::GzDecoder::new(data.as_slice()); |
102 | | - let mut archive = tar::Archive::new(decoder); |
103 | | - |
104 | | - let target_suffix = format!("raw_binaries/{binary_name}"); |
105 | | - |
106 | | - for entry in archive |
107 | | - .entries() |
108 | | - .context("Failed to read tarball entries")? |
109 | | - { |
110 | | - let mut entry = entry.context("Failed to read tarball entry")?; |
111 | | - let path = entry.path().context("Failed to read entry path")?; |
112 | | - let path_str = path.to_string_lossy().to_string(); |
113 | | - |
114 | | - if path_str.ends_with(&target_suffix) { |
115 | | - let mut contents = Vec::new(); |
116 | | - entry |
117 | | - .read_to_end(&mut contents) |
118 | | - .context("Failed to extract odiff binary")?; |
119 | | - |
120 | | - let temp_path = binary_path.with_extension("tmp"); |
121 | | - let mut f = fs::File::create(&temp_path) |
122 | | - .context("Failed to create temp file for odiff binary")?; |
123 | | - f.write_all(&contents) |
124 | | - .context("Failed to write odiff binary")?; |
125 | | - f.sync_all()?; |
126 | | - drop(f); |
127 | | - |
128 | | - #[cfg(unix)] |
129 | | - { |
130 | | - use std::os::unix::fs::PermissionsExt as _; |
131 | | - let mut perm = fs::metadata(&temp_path)?.permissions(); |
132 | | - perm.set_mode(0o755); |
133 | | - fs::set_permissions(&temp_path, perm)?; |
134 | | - } |
| 66 | + eprintln!(); |
| 67 | + eprintln!("Running `npm install -g odiff-bin`..."); |
| 68 | + eprintln!(); |
135 | 69 |
|
136 | | - fs::rename(&temp_path, &binary_path).context("Failed to install odiff binary")?; |
| 70 | + let status = Command::new("npm") |
| 71 | + .args(["install", "-g", "odiff-bin"]) |
| 72 | + .status() |
| 73 | + .context("Failed to run npm. Is npm installed?")?; |
137 | 74 |
|
138 | | - info!( |
139 | | - "odiff {ODIFF_VERSION} installed to {}", |
140 | | - binary_path.display() |
141 | | - ); |
142 | | - return Ok(binary_path); |
143 | | - } |
| 75 | + if !status.success() { |
| 76 | + bail!("npm install failed. Please install odiff manually: npm install -g odiff-bin"); |
144 | 77 | } |
145 | 78 |
|
146 | | - bail!("Could not find {target_suffix} in odiff tarball") |
| 79 | + which::which("odiff").context( |
| 80 | + "odiff was installed but could not be found on PATH. \ |
| 81 | + You may need to restart your shell.", |
| 82 | + ) |
147 | 83 | } |
148 | 84 |
|
149 | 85 | pub fn ensure_binary() -> Result<PathBuf> { |
150 | 86 | if let Ok(system_path) = which::which("odiff") { |
151 | 87 | info!("Using system odiff at {}", system_path.display()); |
| 88 | + check_version(&system_path); |
152 | 89 | return Ok(system_path); |
153 | 90 | } |
154 | 91 |
|
155 | | - let binary_name = platform_binary_name()?; |
156 | | - let cache = cache_dir()?; |
157 | | - let cached_binary = cache.join(binary_name); |
158 | | - if cached_binary.exists() { |
159 | | - info!("Using cached odiff at {}", cached_binary.display()); |
160 | | - return Ok(cached_binary); |
161 | | - } |
162 | | - |
163 | | - download_and_extract() |
| 92 | + prompt_npm_install() |
164 | 93 | } |
165 | 94 |
|
166 | 95 | #[cfg(test)] |
167 | 96 | mod tests { |
168 | 97 | use super::*; |
169 | 98 |
|
170 | 99 | #[test] |
171 | | - fn test_platform_binary_name_returns_value() { |
172 | | - let name = platform_binary_name().expect("should return a binary name"); |
173 | | - assert!( |
174 | | - name.starts_with("odiff-"), |
175 | | - "binary name should start with 'odiff-', got: {name}" |
176 | | - ); |
177 | | - } |
178 | | - |
179 | | - #[test] |
180 | | - fn test_cache_dir_is_under_home() { |
181 | | - let dir = cache_dir().expect("should return a cache dir"); |
182 | | - let home = dirs::home_dir().expect("should have a home dir"); |
183 | | - assert!(dir.starts_with(&home), "cache dir should be under home dir"); |
184 | | - let dir_str = dir.to_string_lossy(); |
185 | | - assert!( |
186 | | - dir_str.ends_with(ODIFF_VERSION), |
187 | | - "cache dir should end with version, got: {dir_str}" |
188 | | - ); |
| 100 | + fn test_min_version_parses() { |
| 101 | + semver::Version::parse(MIN_SUPPORTED_VERSION) |
| 102 | + .expect("MIN_SUPPORTED_VERSION should be valid semver"); |
189 | 103 | } |
190 | 104 | } |
0 commit comments