Skip to content

Commit cf94ea1

Browse files
ref(snapshots): Address PR review feedback
- Replace self-managed odiff binary download with npm install prompt. In interactive mode, offers to run `npm install -g odiff-bin`. In non-interactive mode (CI), errors with clear install instructions. - Move `sentry-cli build snapshots` to `sentry-cli snapshots upload`. The old path is kept as a hidden deprecated alias with a warning. - Add Emerge Tools as CODEOWNERS for snapshots and odiff directories. - Remove orphaned `tar` dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 63fd855 commit cf94ea1

13 files changed

Lines changed: 102 additions & 199 deletions

File tree

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
/src/api/data_types/snapshots.rs @getsentry/emerge-tools @getsentry/owners-sentry-cli
1717
/src/commands/build @getsentry/emerge-tools @getsentry/owners-sentry-cli
1818
/src/utils/build @getsentry/emerge-tools @getsentry/owners-sentry-cli
19+
/src/commands/snapshots @getsentry/emerge-tools @getsentry/owners-sentry-cli
20+
/src/utils/odiff @getsentry/emerge-tools @getsentry/owners-sentry-cli
1921
/tests/integration/build @getsentry/emerge-tools @getsentry/owners-sentry-cli
2022

2123
# Files without codeowner protection

Cargo.lock

Lines changed: 0 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ sha1_smol = { version = "1.0.0", features = ["serde", "std"] }
6969
sha2 = "0.10.9"
7070
sourcemap = { version = "9.3.0", features = ["ram_bundle"] }
7171
symbolic = { version = "12.13.3", features = ["debuginfo-serde", "il2cpp"] }
72-
tar = "0.4"
7372
thiserror = "1.0.38"
7473
tokio = { version = "1.47", features = ["rt"] }
7574
url = "2.3.1"

src/commands/build/mod.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ use clap::{ArgMatches, Command};
44
use crate::utils::args::ArgExt as _;
55

66
pub mod download;
7-
pub mod snapshots;
87
pub mod upload;
98

109
macro_rules! each_subcommand {
1110
($mac:ident) => {
1211
$mac!(download);
13-
$mac!(snapshots);
1412
$mac!(upload);
1513
};
1614
}
@@ -31,6 +29,11 @@ pub fn make_command(mut command: Command) -> Command {
3129
.org_arg()
3230
.project_arg(true);
3331
each_subcommand!(add_subcommand);
32+
command = command.subcommand(
33+
crate::commands::snapshots::upload::make_command(Command::new("snapshots"))
34+
.hide(true)
35+
.about("[DEPRECATED] Use `sentry-cli snapshots upload` instead."),
36+
);
3437
command
3538
}
3639

@@ -45,5 +48,12 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
4548
}};
4649
}
4750
each_subcommand!(execute_subcommand);
51+
if let Some(sub_matches) = matches.subcommand_matches("snapshots") {
52+
eprintln!(
53+
"WARNING: `sentry-cli build snapshots` is deprecated. \
54+
Use `sentry-cli snapshots upload` instead."
55+
);
56+
return crate::commands::snapshots::upload::execute(sub_matches);
57+
}
4858
unreachable!();
4959
}

src/commands/snapshots/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ use anyhow::Result;
22
use clap::{ArgMatches, Command};
33

44
pub mod diff;
5+
pub mod upload;
56

67
macro_rules! each_subcommand {
78
($mac:ident) => {
89
$mac!(diff);
10+
$mac!(upload);
911
};
1012
}
1113

@@ -19,7 +21,7 @@ pub fn make_command(mut command: Command) -> Command {
1921
}
2022

2123
command = command
22-
.about("[EXPERIMENTAL] Manage and compare snapshots locally.")
24+
.about("[EXPERIMENTAL] Manage and compare snapshots.")
2325
.subcommand_required(true)
2426
.arg_required_else_help(true);
2527
each_subcommand!(add_subcommand);
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ use crate::utils::ci::is_ci;
2626
use crate::utils::fs::IMAGE_EXTENSIONS;
2727

2828
const EXPERIMENTAL_WARNING: &str =
29-
"[EXPERIMENTAL] The \"build snapshots\" command is experimental. \
29+
"[EXPERIMENTAL] The \"snapshots upload\" command is experimental. \
3030
The command is subject to breaking changes, including removal, in any Sentry CLI release.";
3131
const MAX_PIXELS_PER_IMAGE: u64 = 40_000_000;
3232

3333
pub fn make_command(command: Command) -> Command {
3434
command
35-
.about("[EXPERIMENTAL] Upload build snapshots to a project.")
35+
.about("[EXPERIMENTAL] Upload snapshots to a project.")
3636
.long_about(format!(
37-
"Upload build snapshots to a project.\n\n{EXPERIMENTAL_WARNING}"
37+
"Upload snapshots to a project.\n\n{EXPERIMENTAL_WARNING}"
3838
))
3939
.org_arg()
4040
.project_arg(false)

src/utils/odiff/binary.rs

Lines changed: 68 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,190 +1,104 @@
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;
44

55
use anyhow::{bail, Context as _, Result};
66
use log::info;
7-
use sha2::{Digest as _, Sha256};
87

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+
}
5831
}
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.");
6434
}
65-
handle.ssl_options(&ssl_opts)?;
6635
}
36+
}
6737

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+
);
8245
}
8346

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)?;
8858

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") {
9660
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"
9863
);
9964
}
10065

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!();
13569

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?")?;
13774

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");
14477
}
14578

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+
)
14783
}
14884

14985
pub fn ensure_binary() -> Result<PathBuf> {
15086
if let Ok(system_path) = which::which("odiff") {
15187
info!("Using system odiff at {}", system_path.display());
88+
check_version(&system_path);
15289
return Ok(system_path);
15390
}
15491

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()
16493
}
16594

16695
#[cfg(test)]
16796
mod tests {
16897
use super::*;
16998

17099
#[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");
189103
}
190104
}

0 commit comments

Comments
 (0)