Skip to content

Commit 7dd4e86

Browse files
committed
PR feedback
1 parent 9daae6d commit 7dd4e86

File tree

5 files changed

+147
-134
lines changed

5 files changed

+147
-134
lines changed

src/commands/mobile_app/upload.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,26 @@ use crate::config::Config;
2121
use crate::utils::args::ArgExt;
2222
use crate::utils::chunks::{upload_chunks, Chunk, ASSEMBLE_POLL_INTERVAL};
2323
use crate::utils::fs::get_sha1_checksums;
24-
use crate::utils::fs::TempFile;
24+
use crate::utils::fs::{TempDir, TempFile};
2525
#[cfg(target_os = "macos")]
26-
use crate::utils::mobile_app::handle_asset_catalogs;
27-
use crate::utils::mobile_app::{
28-
ipa_to_xcarchive, is_aab_file, is_apk_file, is_apple_app, is_ipa_file, is_zip_file,
29-
};
26+
use crate::utils::mobile_app::{handle_asset_catalogs, ipa_to_xcarchive};
27+
use crate::utils::mobile_app::{is_aab_file, is_apk_file, is_apple_app, is_ipa_file, is_zip_file};
3028
use crate::utils::progress::ProgressBar;
3129
use crate::utils::vcs;
3230

3331
pub fn make_command(command: Command) -> Command {
32+
#[cfg(target_os = "macos")]
33+
let help_text = "The path to the mobile app files to upload. Supported files include Apk, Aab, XCArchive, and IPA.";
34+
#[cfg(not(target_os = "macos"))]
35+
let help_text = "The path to the mobile app files to upload. Supported files include Apk, Aab, and XCArchive.";
3436
command
3537
.about("[EXPERIMENTAL] Upload mobile app files to a project.")
3638
.org_arg()
3739
.project_arg(false)
3840
.arg(
3941
Arg::new("paths")
4042
.value_name("PATH")
41-
.help("The path to the mobile app files to upload. Supported files include Apk, Aab, XCArchive, and IPA.")
43+
.help(help_text)
4244
.num_args(1..)
4345
.action(ArgAction::Append)
4446
.required(true),
@@ -99,9 +101,10 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
99101
debug!("Normalizing file: {}", path.display());
100102

101103
// Handle IPA files by converting them to XCArchive
104+
#[cfg(target_os = "macos")]
102105
if is_zip_file(&byteview) && is_ipa_file(&byteview)? {
103106
debug!("Converting IPA file to XCArchive structure");
104-
let temp_dir = crate::utils::fs::TempDir::create()?;
107+
let temp_dir = TempDir::create()?;
105108
ipa_to_xcarchive(path, &byteview, &temp_dir)
106109
.and_then(|path| normalize_directory(&path))
107110
.with_context(|| format!("Failed to process IPA file {}", path.display()))?
@@ -211,15 +214,22 @@ fn validate_is_mobile_app(path: &Path, bytes: &[u8]) -> Result<()> {
211214
return Ok(());
212215
}
213216

217+
#[cfg(target_os = "macos")]
214218
if is_ipa_file(bytes)? {
215219
debug!("Detected IPA file");
216220
return Ok(());
217221
}
218222
}
219223

220224
debug!("File format validation failed");
225+
#[cfg(target_os = "macos")]
226+
let format_list = "APK, AAB, XCArchive, or IPA";
227+
#[cfg(not(target_os = "macos"))]
228+
let format_list = "APK, AAB, or XCArchive";
229+
221230
Err(anyhow!(
222-
"File is not a recognized mobile app format (APK, AAB, XCArchive, or IPA): {}",
231+
"File is not a recognized mobile app format ({}): {}",
232+
format_list,
223233
path.display()
224234
))
225235
}
@@ -450,7 +460,7 @@ mod tests {
450460

451461
#[test]
452462
fn test_normalize_directory_preserves_top_level_directory_name() -> Result<()> {
453-
let temp_dir = crate::utils::fs::TempDir::create()?;
463+
let temp_dir = TempDir::create()?;
454464
let test_dir = temp_dir.path().join("MyApp.xcarchive");
455465
fs::create_dir_all(test_dir.join("Products"))?;
456466
fs::write(test_dir.join("Products").join("app.txt"), "test content")?;

src/utils/mobile_app/apple.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
use anyhow::{anyhow, Result};
2+
use log::debug;
3+
use regex::Regex;
14
use std::path::{Path, PathBuf};
25

6+
use crate::utils::fs::TempDir;
37
use apple_catalog_parsing;
8+
use std::io::Cursor;
49
use walkdir::WalkDir;
10+
use zip::ZipArchive;
511

612
pub fn handle_asset_catalogs(path: &Path) {
713
// Find all asset catalogs
@@ -27,3 +33,121 @@ fn find_car_files(root: &Path) -> Vec<PathBuf> {
2733
.map(|e| e.into_path())
2834
.collect()
2935
}
36+
37+
/// Converts an IPA file to an XCArchive directory structure. The provided IPA must be a valid IPA file.
38+
///
39+
/// # Format Overview
40+
///
41+
/// ## IPA (iOS App Store Package)
42+
/// An IPA file is a compressed archive containing an iOS app ready for distribution.
43+
/// It has the following structure:
44+
/// ```
45+
/// MyApp.ipa
46+
/// └── Payload/
47+
/// └── MyApp.app/
48+
/// ├── Info.plist
49+
/// ├── MyApp (executable)
50+
/// ├── Assets.car
51+
/// └── ... (other app resources)
52+
/// ```
53+
///
54+
/// ## XCArchive (Xcode Archive)
55+
/// An XCArchive is a directory structure created by Xcode when archiving an app for distribution.
56+
/// It has the following structure:
57+
/// ```
58+
/// MyApp.xcarchive/
59+
/// ├── Info.plist
60+
/// ├── Products/
61+
/// │ └── Applications/
62+
/// │ └── MyApp.app/
63+
/// │ ├── Info.plist
64+
/// │ ├── MyApp (executable)
65+
/// │ ├── Assets.car
66+
/// │ └── ... (other app resources)
67+
/// └── ... (other archive metadata)
68+
/// ```
69+
pub fn ipa_to_xcarchive(ipa_path: &Path, ipa_bytes: &[u8], temp_dir: &TempDir) -> Result<PathBuf> {
70+
debug!(
71+
"Converting IPA to XCArchive structure: {}",
72+
ipa_path.display()
73+
);
74+
75+
let xcarchive_dir = temp_dir.path().join("archive.xcarchive");
76+
let products_dir = xcarchive_dir.join("Products");
77+
let applications_dir = products_dir.join("Applications");
78+
79+
debug!("Creating XCArchive directory structure");
80+
std::fs::create_dir_all(&applications_dir)?;
81+
82+
// Extract IPA file
83+
let cursor = Cursor::new(ipa_bytes);
84+
let mut ipa_archive = ZipArchive::new(cursor)?;
85+
86+
let app_name = extract_app_name_from_ipa(&ipa_archive)?;
87+
88+
// Extract all files from the archive
89+
for i in 0..ipa_archive.len() {
90+
let mut file = ipa_archive.by_index(i)?;
91+
92+
if let Some(name) = file.enclosed_name() {
93+
if let Ok(stripped) = name.strip_prefix("Payload/") {
94+
if !file.is_dir() {
95+
// Create the file path in the XCArchive structure
96+
let target_path = applications_dir.join(stripped);
97+
98+
// Create parent directories if necessary
99+
if let Some(parent) = target_path.parent() {
100+
std::fs::create_dir_all(parent)?;
101+
}
102+
103+
// Extract file
104+
let mut target_file = std::fs::File::create(&target_path)?;
105+
std::io::copy(&mut file, &mut target_file)?;
106+
}
107+
}
108+
}
109+
}
110+
111+
// Create Info.plist for XCArchive
112+
let info_plist_path = xcarchive_dir.join("Info.plist");
113+
114+
let info_plist_content = format!(
115+
r#"<?xml version="1.0" encoding="UTF-8"?>
116+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
117+
<plist version="1.0">
118+
<dict>
119+
<key>ApplicationProperties</key>
120+
<dict>
121+
<key>ApplicationPath</key>
122+
<string>Applications/{app_name}.app</string>
123+
</dict>
124+
<key>ArchiveVersion</key>
125+
<integer>1</integer>
126+
</dict>
127+
</plist>"#
128+
);
129+
130+
std::fs::write(&info_plist_path, info_plist_content)?;
131+
132+
debug!(
133+
"Created XCArchive Info.plist at: {}",
134+
info_plist_path.display()
135+
);
136+
Ok(xcarchive_dir)
137+
}
138+
139+
fn extract_app_name_from_ipa(archive: &ZipArchive<Cursor<&[u8]>>) -> Result<String> {
140+
let pattern = Regex::new(r"^Payload/([^/]+)\.app/Info\.plist$")?;
141+
let mut matches = Vec::new();
142+
143+
for name in archive.file_names() {
144+
if let Some(captures) = pattern.captures(name) {
145+
matches.push(captures[1].to_string());
146+
}
147+
}
148+
149+
match matches.len() {
150+
1 => Ok(matches[0].clone()),
151+
_ => Err(anyhow!("IPA file did not contain exactly one .app.")),
152+
}
153+
}

src/utils/mobile_app/ipa.rs

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1 @@
1-
#![cfg(feature = "unstable-mobile-app")]
21

3-
use anyhow::{anyhow, Result};
4-
use log::debug;
5-
use std::io::Cursor;
6-
use std::path::{Path, PathBuf};
7-
use zip::ZipArchive;
8-
9-
use crate::utils::fs::TempDir;
10-
11-
/// Converts an IPA file to an XCArchive directory structure. The provided IPA must be a valid IPA file.
12-
///
13-
/// # Format Overview
14-
///
15-
/// ## IPA (iOS App Store Package)
16-
/// An IPA file is a compressed archive containing an iOS app ready for distribution.
17-
/// It has the following structure:
18-
/// ```
19-
/// MyApp.ipa
20-
/// └── Payload/
21-
/// └── MyApp.app/
22-
/// ├── Info.plist
23-
/// ├── MyApp (executable)
24-
/// ├── Assets.car
25-
/// └── ... (other app resources)
26-
/// ```
27-
///
28-
/// ## XCArchive (Xcode Archive)
29-
/// An XCArchive is a directory structure created by Xcode when archiving an app for distribution.
30-
/// It has the following structure:
31-
/// ```
32-
/// MyApp.xcarchive/
33-
/// ├── Info.plist
34-
/// ├── Products/
35-
/// │ └── Applications/
36-
/// │ └── MyApp.app/
37-
/// │ ├── Info.plist
38-
/// │ ├── MyApp (executable)
39-
/// │ ├── Assets.car
40-
/// │ └── ... (other app resources)
41-
/// └── ... (other archive metadata)
42-
/// ```
43-
pub fn ipa_to_xcarchive(ipa_path: &Path, ipa_bytes: &[u8], temp_dir: &TempDir) -> Result<PathBuf> {
44-
debug!(
45-
"Converting IPA to XCArchive structure: {}",
46-
ipa_path.display()
47-
);
48-
49-
let xcarchive_dir = temp_dir.path().join("archive.xcarchive");
50-
let products_dir = xcarchive_dir.join("Products");
51-
let applications_dir = products_dir.join("Applications");
52-
53-
debug!("Creating XCArchive directory structure");
54-
std::fs::create_dir_all(&applications_dir)?;
55-
56-
// Extract IPA file
57-
let cursor = Cursor::new(ipa_bytes);
58-
let mut ipa_archive = ZipArchive::new(cursor)?;
59-
60-
let app_name = extract_app_name_from_ipa(&ipa_archive)?;
61-
62-
// Extract all files from the archive
63-
for i in 0..ipa_archive.len() {
64-
let mut file = ipa_archive.by_index(i)?;
65-
66-
if let Some(stripped) = file
67-
.enclosed_name()
68-
.and_then(|name| name.strip_prefix("Payload/").ok())
69-
{
70-
if !file.is_dir() {
71-
// Create the file path in the XCArchive structure
72-
let target_path = applications_dir.join(stripped);
73-
74-
// Create parent directories if necessary
75-
if let Some(parent) = target_path.parent() {
76-
std::fs::create_dir_all(parent)?;
77-
}
78-
79-
// Extract file
80-
let mut target_file = std::fs::File::create(&target_path)?;
81-
std::io::copy(&mut file, &mut target_file)?;
82-
}
83-
}
84-
}
85-
}
86-
87-
// Create Info.plist for XCArchive
88-
let info_plist_path = xcarchive_dir.join("Info.plist");
89-
90-
let info_plist_content = format!(
91-
r#"<?xml version="1.0" encoding="UTF-8"?>
92-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
93-
<plist version="1.0">
94-
<dict>
95-
<key>ApplicationProperties</key>
96-
<dict>
97-
<key>ApplicationPath</key>
98-
<string>Applications/{app_name}.app</string>
99-
</dict>
100-
<key>ArchiveVersion</key>
101-
<integer>1</integer>
102-
</dict>
103-
</plist>"#
104-
);
105-
106-
std::fs::write(&info_plist_path, info_plist_content)?;
107-
108-
debug!(
109-
"Created XCArchive Info.plist at: {}",
110-
info_plist_path.display()
111-
);
112-
Ok(xcarchive_dir)
113-
}
114-
115-
fn extract_app_name_from_ipa<'a>(archive: &'a ZipArchive<Cursor<&[u8]>>) -> Result<&'a str> {
116-
archive
117-
.file_names()
118-
.filter(|name| name.starts_with("Payload/") && name.ends_with(".app/Info.plist"))
119-
.min_by_key(|name| name.len())
120-
.and_then(|name| name.strip_prefix("Payload/"))
121-
.and_then(|name| name.split('/').next())
122-
.and_then(|name| name.strip_suffix(".app"))
123-
.ok_or_else(|| anyhow!("No .app found in IPA"))
124-
}

src/utils/mobile_app/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
#[cfg(target_os = "macos")]
44
mod apple;
5+
#[cfg(target_os = "macos")]
56
mod ipa;
67
mod validation;
78

89
#[cfg(target_os = "macos")]
910
pub use self::apple::handle_asset_catalogs;
10-
pub use self::ipa::ipa_to_xcarchive;
11+
#[cfg(target_os = "macos")]
12+
pub use self::apple::ipa_to_xcarchive;
1113
pub use self::validation::{is_aab_file, is_apk_file, is_apple_app, is_ipa_file, is_zip_file};

tests/integration/_cases/mobile_app/mobile_app-upload-help.trycmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Usage: sentry-cli[EXE] mobile-app upload [OPTIONS] <PATH>...
77

88
Arguments:
99
<PATH>... The path to the mobile app files to upload. Supported files include Apk, Aab,
10-
XCArchive, or IPA.
10+
XCArchive, and IPA.
1111

1212
Options:
1313
-o, --org <ORG>

0 commit comments

Comments
 (0)