Dadoum commited on 2026-07-10 00:55:28
Showing 11 changed files, with 270 additions and 39 deletions.
| ... | ... |
@@ -461,7 +461,7 @@ pub(crate) async fn sign_ipa( |
| 461 | 461 |
)]); |
| 462 | 462 |
let mut profile_requests = vec![(effective_bundle_id.clone(), app_id.developer_id)]; |
| 463 | 463 |
|
| 464 |
- for (nested_bundle, target) in request.app.nested_bundles.iter().zip(targets.nested) {
|
|
| 464 |
+ for (nested_bundle, target) in request.app.nested_bundles_for_signing().zip(targets.nested) {
|
|
| 465 | 465 |
let (nested_app_id, account_update) = resolve_signing_app_id( |
| 466 | 466 |
request.developer_context.clone(), |
| 467 | 467 |
&request.team_id, |
| ... | ... |
@@ -521,6 +521,7 @@ pub(crate) async fn sign_ipa( |
| 521 | 521 |
metadata, |
| 522 | 522 |
bundle_id_replacements, |
| 523 | 523 |
icon_override_path: request.app.icon_override_path.as_deref().map(PathBuf::from), |
| 524 |
+ strip_extensions: request.app.strip_extensions, |
|
| 524 | 525 |
team_id: request.team_id, |
| 525 | 526 |
provisioning_profiles, |
| 526 | 527 |
private_key_pem: signing_material.private_key_pem, |
| ... | ... |
@@ -690,8 +691,7 @@ fn nested_app_id_targets( |
| 690 | 691 |
app: &AppOption, |
| 691 | 692 |
root_bundle_identifier: &str, |
| 692 | 693 |
) -> Vec<SigningAppIdTarget> {
|
| 693 |
- app.nested_bundles |
|
| 694 |
- .iter() |
|
| 694 |
+ app.nested_bundles_for_signing() |
|
| 695 | 695 |
.map(|nested_bundle| {
|
| 696 | 696 |
let desired_identifier = rebased_nested_bundle_identifier( |
| 697 | 697 |
app.original_bundle_id(), |
| ... | ... |
@@ -1037,7 +1037,8 @@ pub(crate) async fn download_provisioning_profile( |
| 1037 | 1037 |
mod tests {
|
| 1038 | 1038 |
use super::*; |
| 1039 | 1039 |
use crate::app::models::{
|
| 1040 |
- AppMetadata, EntitlementsSource, NestedBundleOption, SupportedDeviceFamily, |
|
| 1040 |
+ AppMetadata, EntitlementsSource, NestedBundleKind, NestedBundleOption, |
|
| 1041 |
+ SupportedDeviceFamily, |
|
| 1041 | 1042 |
}; |
| 1042 | 1043 |
|
| 1043 | 1044 |
fn app_id(identifier: &str) -> AppIdOption {
|
| ... | ... |
@@ -1066,8 +1067,10 @@ mod tests {
|
| 1066 | 1067 |
.map(|bundle_id| NestedBundleOption {
|
| 1067 | 1068 |
name: bundle_id.rsplit('.').next().unwrap().to_string(),
|
| 1068 | 1069 |
bundle_id: (*bundle_id).to_string(), |
| 1070 |
+ kind: NestedBundleKind::AppExtension, |
|
| 1069 | 1071 |
}) |
| 1070 | 1072 |
.collect(), |
| 1073 |
+ strip_extensions: false, |
|
| 1071 | 1074 |
path: "/tmp/Example.ipa".to_string(), |
| 1072 | 1075 |
icon_path: None, |
| 1073 | 1076 |
icon_override_path: None, |
| ... | ... |
@@ -1186,6 +1189,26 @@ mod tests {
|
| 1186 | 1189 |
assert_eq!(plan.remaining_after_signing(), Some(3)); |
| 1187 | 1190 |
} |
| 1188 | 1191 |
|
| 1192 |
+ #[test] |
|
| 1193 |
+ fn provisioning_plan_omits_stripped_extensions_but_keeps_nested_apps() {
|
|
| 1194 |
+ let mut app = app( |
|
| 1195 |
+ "com.example.app", |
|
| 1196 |
+ &["com.example.app.widget", "com.example.app.watch"], |
|
| 1197 |
+ ); |
|
| 1198 |
+ app.nested_bundles[1].kind = NestedBundleKind::App; |
|
| 1199 |
+ app.strip_extensions = true; |
|
| 1200 |
+ |
|
| 1201 |
+ let plan = app_id_provisioning_plan(&team(Vec::new(), Some(5)), true, None, &app).unwrap(); |
|
| 1202 |
+ |
|
| 1203 |
+ assert_eq!( |
|
| 1204 |
+ plan.app_ids |
|
| 1205 |
+ .iter() |
|
| 1206 |
+ .map(|app_id| app_id.identifier.as_str()) |
|
| 1207 |
+ .collect::<Vec<_>>(), |
|
| 1208 |
+ vec!["com.example.app.TEAM", "com.example.app.TEAM.watch"] |
|
| 1209 |
+ ); |
|
| 1210 |
+ } |
|
| 1211 |
+ |
|
| 1189 | 1212 |
#[test] |
| 1190 | 1213 |
fn provisioning_plan_reuses_unchanged_registered_app_ids() {
|
| 1191 | 1214 |
let app = app("com.example.app", &["com.example.app.widget"]);
|
| ... | ... |
@@ -437,6 +437,7 @@ pub(crate) enum EntitlementsSource {
|
| 437 | 437 |
pub(crate) struct AppOption {
|
| 438 | 438 |
pub(crate) metadata: AppMetadata, |
| 439 | 439 |
pub(crate) nested_bundles: Vec<NestedBundleOption>, |
| 440 |
+ pub(crate) strip_extensions: bool, |
|
| 440 | 441 |
pub(crate) path: String, |
| 441 | 442 |
pub(crate) icon_path: Option<String>, |
| 442 | 443 |
pub(crate) icon_override_path: Option<String>, |
| ... | ... |
@@ -450,6 +451,13 @@ pub(crate) struct AppOption {
|
| 450 | 451 |
pub(crate) struct NestedBundleOption {
|
| 451 | 452 |
pub(crate) name: String, |
| 452 | 453 |
pub(crate) bundle_id: String, |
| 454 |
+ pub(crate) kind: NestedBundleKind, |
|
| 455 |
+} |
|
| 456 |
+ |
|
| 457 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 458 |
+pub(crate) enum NestedBundleKind {
|
|
| 459 |
+ App, |
|
| 460 |
+ AppExtension, |
|
| 453 | 461 |
} |
| 454 | 462 |
|
| 455 | 463 |
impl AppOption {
|
| ... | ... |
@@ -473,6 +481,19 @@ impl AppOption {
|
| 473 | 481 |
self.metadata.build.value() |
| 474 | 482 |
} |
| 475 | 483 |
|
| 484 |
+ pub(crate) fn app_extension_count(&self) -> usize {
|
|
| 485 |
+ self.nested_bundles |
|
| 486 |
+ .iter() |
|
| 487 |
+ .filter(|bundle| bundle.kind == NestedBundleKind::AppExtension) |
|
| 488 |
+ .count() |
|
| 489 |
+ } |
|
| 490 |
+ |
|
| 491 |
+ pub(crate) fn nested_bundles_for_signing(&self) -> impl Iterator<Item = &NestedBundleOption> {
|
|
| 492 |
+ self.nested_bundles.iter().filter(|bundle| {
|
|
| 493 |
+ !self.strip_extensions || bundle.kind != NestedBundleKind::AppExtension |
|
| 494 |
+ }) |
|
| 495 |
+ } |
|
| 496 |
+ |
|
| 476 | 497 |
pub(crate) fn field(&self, field: AppMetadataField) -> &AppFieldValue {
|
| 477 | 498 |
self.metadata.field(field) |
| 478 | 499 |
} |
| ... | ... |
@@ -89,6 +89,8 @@ pub(crate) struct AppOverridePreferences {
|
| 89 | 89 |
pub(crate) supported_devices: Option<String>, |
| 90 | 90 |
pub(crate) icon_path: Option<String>, |
| 91 | 91 |
pub(crate) entitlements: Option<Vec<EntitlementPreferences>>, |
| 92 |
+ #[serde(default)] |
|
| 93 |
+ pub(crate) strip_extensions: bool, |
|
| 92 | 94 |
} |
| 93 | 95 |
|
| 94 | 96 |
#[derive(Clone, Debug, Deserialize, Serialize)] |
| ... | ... |
@@ -255,6 +257,7 @@ impl From<&AppOption> for AppOverridePreferences {
|
| 255 | 257 |
.map(EntitlementPreferences::from) |
| 256 | 258 |
.collect() |
| 257 | 259 |
}), |
| 260 |
+ strip_extensions: app.strip_extensions, |
|
| 258 | 261 |
} |
| 259 | 262 |
} |
| 260 | 263 |
} |
| ... | ... |
@@ -277,6 +280,7 @@ pub(crate) fn apply_app_overrides(app: &mut AppOption, overrides: &AppOverridePr |
| 277 | 280 |
.map(AppEntitlement::from) |
| 278 | 281 |
.collect::<Vec<_>>() |
| 279 | 282 |
}); |
| 283 |
+ app.strip_extensions = overrides.strip_extensions && app.app_extension_count() > 0; |
|
| 280 | 284 |
} |
| 281 | 285 |
|
| 282 | 286 |
pub(crate) fn load_preferences() -> AppResult<SideloaderPreferences> {
|
| ... | ... |
@@ -299,7 +303,9 @@ pub(crate) fn save_preferences(preferences: &SideloaderPreferences) -> AppResult |
| 299 | 303 |
#[cfg(test)] |
| 300 | 304 |
mod tests {
|
| 301 | 305 |
use super::*; |
| 302 |
- use crate::app::models::{AppMetadata, EntitlementsSource, PatchOption};
|
|
| 306 |
+ use crate::app::models::{
|
|
| 307 |
+ AppMetadata, EntitlementsSource, NestedBundleKind, NestedBundleOption, PatchOption, |
|
| 308 |
+ }; |
|
| 303 | 309 |
|
| 304 | 310 |
fn sample_app() -> AppOption {
|
| 305 | 311 |
AppOption {
|
| ... | ... |
@@ -312,7 +318,12 @@ mod tests {
|
| 312 | 318 |
"16.0", |
| 313 | 319 |
vec![SupportedDeviceFamily::IPhone], |
| 314 | 320 |
), |
| 315 |
- nested_bundles: Vec::new(), |
|
| 321 |
+ nested_bundles: vec![NestedBundleOption {
|
|
| 322 |
+ name: "Widget".to_string(), |
|
| 323 |
+ bundle_id: "com.example.original.widget".to_string(), |
|
| 324 |
+ kind: NestedBundleKind::AppExtension, |
|
| 325 |
+ }], |
|
| 326 |
+ strip_extensions: false, |
|
| 316 | 327 |
path: "/tmp/App.ipa".to_string(), |
| 317 | 328 |
icon_path: None, |
| 318 | 329 |
icon_override_path: None, |
| ... | ... |
@@ -346,6 +357,7 @@ mod tests {
|
| 346 | 357 |
value: "true".to_string(), |
| 347 | 358 |
values: None, |
| 348 | 359 |
}]), |
| 360 |
+ strip_extensions: true, |
|
| 349 | 361 |
}, |
| 350 | 362 |
); |
| 351 | 363 |
|
| ... | ... |
@@ -360,6 +372,8 @@ mod tests {
|
| 360 | 372 |
&[SupportedDeviceFamily::IPhone, SupportedDeviceFamily::IPad] |
| 361 | 373 |
); |
| 362 | 374 |
assert_eq!(app.icon_override_path.as_deref(), Some("/tmp/icon.png"));
|
| 375 |
+ assert!(app.strip_extensions); |
|
| 376 |
+ assert!(AppOverridePreferences::from(&app).strip_extensions); |
|
| 363 | 377 |
assert_eq!( |
| 364 | 378 |
app.entitlement_overrides, |
| 365 | 379 |
Some(vec![AppEntitlement {
|
| ... | ... |
@@ -368,4 +382,11 @@ mod tests {
|
| 368 | 382 |
}]) |
| 369 | 383 |
); |
| 370 | 384 |
} |
| 385 |
+ |
|
| 386 |
+ #[test] |
|
| 387 |
+ fn older_app_overrides_default_to_preserving_extensions() {
|
|
| 388 |
+ let overrides = toml::from_str::<AppOverridePreferences>("").unwrap();
|
|
| 389 |
+ |
|
| 390 |
+ assert!(!overrides.strip_extensions); |
|
| 391 |
+ } |
|
| 371 | 392 |
} |
| ... | ... |
@@ -2,8 +2,8 @@ use crate::app::models::{
|
| 2 | 2 |
AccountOption, AdiBackendAvailability, AdiBackendDetail, AdiBackendKind, AdiBackendOption, |
| 3 | 3 |
AdiProvisioningState, AdiRepairAction, AppEntitlement, AppIdCapabilityOption, AppIdOption, |
| 4 | 4 |
AppMetadata, AppOption, DeveloperDeviceOption, DevelopmentCertificateOption, DeviceOption, |
| 5 |
- EntitlementValue, EntitlementsSource, MachineIdentity, NestedBundleOption, PatchOption, |
|
| 6 |
- SupportedDeviceFamily, TeamOption, |
|
| 5 |
+ EntitlementValue, EntitlementsSource, MachineIdentity, NestedBundleKind, NestedBundleOption, |
|
| 6 |
+ PatchOption, SupportedDeviceFamily, TeamOption, |
|
| 7 | 7 |
}; |
| 8 | 8 |
use crate::domain::{
|
| 9 | 9 |
adi as domain_adi, device as domain_device, identity as domain_identity, ipa as domain_ipa, |
| ... | ... |
@@ -100,8 +100,13 @@ pub(crate) fn app_option(app: domain_ipa::IpaApp) -> AppOption {
|
| 100 | 100 |
.map(|bundle| NestedBundleOption {
|
| 101 | 101 |
name: bundle.name, |
| 102 | 102 |
bundle_id: bundle.bundle_id, |
| 103 |
+ kind: match bundle.kind {
|
|
| 104 |
+ domain_ipa::NestedBundleKind::App => NestedBundleKind::App, |
|
| 105 |
+ domain_ipa::NestedBundleKind::AppExtension => NestedBundleKind::AppExtension, |
|
| 106 |
+ }, |
|
| 103 | 107 |
}) |
| 104 | 108 |
.collect(), |
| 109 |
+ strip_extensions: false, |
|
| 105 | 110 |
path: app.path, |
| 106 | 111 |
icon_path: app.icon_path, |
| 107 | 112 |
icon_override_path: None, |
| ... | ... |
@@ -495,6 +500,7 @@ mod tests {
|
| 495 | 500 |
nested_bundles: vec![domain_ipa::NestedBundle {
|
| 496 | 501 |
name: "Widget".to_string(), |
| 497 | 502 |
bundle_id: "com.example.app.Widget".to_string(), |
| 503 |
+ kind: domain_ipa::NestedBundleKind::AppExtension, |
|
| 498 | 504 |
}], |
| 499 | 505 |
path: "/tmp/Example.ipa".to_string(), |
| 500 | 506 |
icon_path: Some("/tmp/icon.png".to_string()),
|
| ... | ... |
@@ -512,6 +518,10 @@ mod tests {
|
| 512 | 518 |
assert_eq!(option.name(), "Example"); |
| 513 | 519 |
assert_eq!(option.bundle_id(), "com.example.app"); |
| 514 | 520 |
assert_eq!(option.nested_bundles[0].bundle_id, "com.example.app.Widget"); |
| 521 |
+ assert_eq!( |
|
| 522 |
+ option.nested_bundles[0].kind, |
|
| 523 |
+ NestedBundleKind::AppExtension |
|
| 524 |
+ ); |
|
| 515 | 525 |
assert_eq!( |
| 516 | 526 |
option.supported_devices(), |
| 517 | 527 |
&[SupportedDeviceFamily::IPhone, SupportedDeviceFamily::IPad] |
| ... | ... |
@@ -5,8 +5,8 @@ use crate::backend::icon::{
|
| 5 | 5 |
use crate::backend::paths::app_data_dir; |
| 6 | 6 |
use crate::backend::{runtime as backend_runtime, BackendError, BackendResult};
|
| 7 | 7 |
use crate::domain::{
|
| 8 |
- AppEntitlement, AppMetadata, EntitlementValue, EntitlementsSource, IpaApp, NestedBundle, Patch, |
|
| 9 |
- SupportedDeviceFamily, |
|
| 8 |
+ AppEntitlement, AppMetadata, EntitlementValue, EntitlementsSource, IpaApp, NestedBundle, |
|
| 9 |
+ NestedBundleKind, Patch, SupportedDeviceFamily, |
|
| 10 | 10 |
}; |
| 11 | 11 |
use apple_codesign::{
|
| 12 | 12 |
sign_bundle, BundleSigningSettings, ProvisioningProfile, RustCryptoCmsSigner, |
| ... | ... |
@@ -42,6 +42,7 @@ pub(crate) struct IpaSigningRequest {
|
| 42 | 42 |
pub(crate) metadata: AppMetadata, |
| 43 | 43 |
pub(crate) bundle_id_replacements: BTreeMap<String, String>, |
| 44 | 44 |
pub(crate) icon_override_path: Option<PathBuf>, |
| 45 |
+ pub(crate) strip_extensions: bool, |
|
| 45 | 46 |
pub(crate) team_id: String, |
| 46 | 47 |
pub(crate) provisioning_profiles: BTreeMap<String, Vec<u8>>, |
| 47 | 48 |
pub(crate) private_key_pem: Vec<u8>, |
| ... | ... |
@@ -162,12 +163,13 @@ async fn read_nested_signable_bundles( |
| 162 | 163 |
.enumerate() |
| 163 | 164 |
.filter_map(|(index, entry)| {
|
| 164 | 165 |
let path = entry_name(entry)?; |
| 165 |
- is_nested_signable_info_plist(path, app_prefix).then(|| (index, path.to_string())) |
|
| 166 |
+ nested_signable_bundle_kind(path, app_prefix) |
|
| 167 |
+ .map(|kind| (index, path.to_string(), kind)) |
|
| 166 | 168 |
}) |
| 167 | 169 |
.collect::<Vec<_>>(); |
| 168 | 170 |
let mut bundles = Vec::with_capacity(candidates.len()); |
| 169 | 171 |
|
| 170 |
- for (index, path) in candidates {
|
|
| 172 |
+ for (index, path, kind) in candidates {
|
|
| 171 | 173 |
let bytes = read_entry(reader, index, &path).await?; |
| 172 | 174 |
let info = plist::from_bytes::<Dictionary>(&bytes) |
| 173 | 175 |
.map_err(|error| BackendError::Plist(format!("{path}: {error}")))?;
|
| ... | ... |
@@ -179,7 +181,11 @@ async fn read_nested_signable_bundles( |
| 179 | 181 |
.or_else(|| plist_string(&info, "CFBundleName")) |
| 180 | 182 |
.unwrap_or(&bundle_id) |
| 181 | 183 |
.to_string(); |
| 182 |
- bundles.push(NestedBundle { name, bundle_id });
|
|
| 184 |
+ bundles.push(NestedBundle {
|
|
| 185 |
+ name, |
|
| 186 |
+ bundle_id, |
|
| 187 |
+ kind, |
|
| 188 |
+ }); |
|
| 183 | 189 |
} |
| 184 | 190 |
|
| 185 | 191 |
bundles.sort_by(|left, right| left.bundle_id.cmp(&right.bundle_id)); |
| ... | ... |
@@ -187,18 +193,23 @@ async fn read_nested_signable_bundles( |
| 187 | 193 |
Ok(bundles) |
| 188 | 194 |
} |
| 189 | 195 |
|
| 190 |
-fn is_nested_signable_info_plist(path: &str, app_prefix: &str) -> bool {
|
|
| 191 |
- let Some(relative) = path |
|
| 196 |
+fn nested_signable_bundle_kind(path: &str, app_prefix: &str) -> Option<NestedBundleKind> {
|
|
| 197 |
+ let relative = path |
|
| 192 | 198 |
.strip_prefix(app_prefix) |
| 193 |
- .and_then(|path| path.strip_suffix("/Info.plist"))
|
|
| 194 |
- else {
|
|
| 195 |
- return false; |
|
| 196 |
- }; |
|
| 197 |
- !relative.is_empty() |
|
| 198 |
- && relative |
|
| 199 |
- .rsplit('/')
|
|
| 200 |
- .next() |
|
| 201 |
- .is_some_and(|folder| folder.ends_with(".appex") || folder.ends_with(".app"))
|
|
| 199 |
+ .and_then(|path| path.strip_suffix("/Info.plist"))?;
|
|
| 200 |
+ if relative.is_empty() {
|
|
| 201 |
+ return None; |
|
| 202 |
+ } |
|
| 203 |
+ let extension = Path::new(relative) |
|
| 204 |
+ .extension() |
|
| 205 |
+ .and_then(|extension| extension.to_str())?; |
|
| 206 |
+ if extension.eq_ignore_ascii_case("appex") {
|
|
| 207 |
+ Some(NestedBundleKind::AppExtension) |
|
| 208 |
+ } else if extension.eq_ignore_ascii_case("app") {
|
|
| 209 |
+ Some(NestedBundleKind::App) |
|
| 210 |
+ } else {
|
|
| 211 |
+ None |
|
| 212 |
+ } |
|
| 202 | 213 |
} |
| 203 | 214 |
|
| 204 | 215 |
fn is_app_info_plist(path: &str) -> bool {
|
| ... | ... |
@@ -470,6 +481,9 @@ async fn sign_ipa_async( |
| 470 | 481 |
extract_ipa(&request.source_path, work_dir.path(), &mut progress).await?; |
| 471 | 482 |
let app_path = root_app_bundle(work_dir.path())?; |
| 472 | 483 |
progress(IpaSigningProgress::Patching); |
| 484 |
+ if request.strip_extensions {
|
|
| 485 |
+ strip_app_extensions(&app_path)?; |
|
| 486 |
+ } |
|
| 473 | 487 |
patch_app_bundle( |
| 474 | 488 |
&app_path, |
| 475 | 489 |
&request.metadata, |
| ... | ... |
@@ -523,6 +537,39 @@ async fn sign_ipa_async( |
| 523 | 537 |
Ok(SigningArtifact::Ipa(output_path)) |
| 524 | 538 |
} |
| 525 | 539 |
|
| 540 |
+fn strip_app_extensions(app_path: &Path) -> BackendResult<()> {
|
|
| 541 |
+ let mut entries = WalkDir::new(app_path).follow_links(false).into_iter(); |
|
| 542 |
+ let mut extension_paths = Vec::new(); |
|
| 543 |
+ |
|
| 544 |
+ while let Some(entry) = entries.next() {
|
|
| 545 |
+ let entry = entry.map_err(|error| {
|
|
| 546 |
+ BackendError::Message(format!( |
|
| 547 |
+ "Failed to inspect app extensions under {}: {error}",
|
|
| 548 |
+ app_path.display() |
|
| 549 |
+ )) |
|
| 550 |
+ })?; |
|
| 551 |
+ if entry.file_type().is_dir() |
|
| 552 |
+ && entry |
|
| 553 |
+ .path() |
|
| 554 |
+ .extension() |
|
| 555 |
+ .and_then(|extension| extension.to_str()) |
|
| 556 |
+ .is_some_and(|extension| extension.eq_ignore_ascii_case("appex"))
|
|
| 557 |
+ {
|
|
| 558 |
+ extension_paths.push(entry.into_path()); |
|
| 559 |
+ entries.skip_current_dir(); |
|
| 560 |
+ } |
|
| 561 |
+ } |
|
| 562 |
+ |
|
| 563 |
+ for path in extension_paths {
|
|
| 564 |
+ fs::remove_dir_all(&path).map_err(|source| BackendError::Io {
|
|
| 565 |
+ action: "Remove app extension", |
|
| 566 |
+ path, |
|
| 567 |
+ source, |
|
| 568 |
+ })?; |
|
| 569 |
+ } |
|
| 570 |
+ Ok(()) |
|
| 571 |
+} |
|
| 572 |
+ |
|
| 526 | 573 |
async fn extract_ipa( |
| 527 | 574 |
source_path: &Path, |
| 528 | 575 |
destination: &Path, |
| ... | ... |
@@ -1474,22 +1521,51 @@ mod tests {
|
| 1474 | 1521 |
fn nested_signable_bundle_paths_include_extensions_and_nested_apps() {
|
| 1475 | 1522 |
let root = "Payload/App.app/"; |
| 1476 | 1523 |
|
| 1477 |
- assert!(is_nested_signable_info_plist( |
|
| 1478 |
- "Payload/App.app/PlugIns/Widget.appex/Info.plist", |
|
| 1479 |
- root |
|
| 1480 |
- )); |
|
| 1481 |
- assert!(is_nested_signable_info_plist( |
|
| 1482 |
- "Payload/App.app/Watch/WatchApp.app/Info.plist", |
|
| 1483 |
- root |
|
| 1484 |
- )); |
|
| 1485 |
- assert!(!is_nested_signable_info_plist( |
|
| 1524 |
+ assert_eq!( |
|
| 1525 |
+ nested_signable_bundle_kind("Payload/App.app/PlugIns/Widget.appex/Info.plist", root),
|
|
| 1526 |
+ Some(NestedBundleKind::AppExtension) |
|
| 1527 |
+ ); |
|
| 1528 |
+ assert_eq!( |
|
| 1529 |
+ nested_signable_bundle_kind("Payload/App.app/Watch/WatchApp.app/Info.plist", root),
|
|
| 1530 |
+ Some(NestedBundleKind::App) |
|
| 1531 |
+ ); |
|
| 1532 |
+ assert_eq!( |
|
| 1533 |
+ nested_signable_bundle_kind( |
|
| 1486 | 1534 |
"Payload/App.app/Frameworks/Example.framework/Info.plist", |
| 1487 | 1535 |
root |
| 1488 |
- )); |
|
| 1489 |
- assert!(!is_nested_signable_info_plist( |
|
| 1490 |
- "Payload/App.app/Info.plist", |
|
| 1491 |
- root |
|
| 1492 |
- )); |
|
| 1536 |
+ ), |
|
| 1537 |
+ None |
|
| 1538 |
+ ); |
|
| 1539 |
+ assert_eq!( |
|
| 1540 |
+ nested_signable_bundle_kind("Payload/App.app/Info.plist", root),
|
|
| 1541 |
+ None |
|
| 1542 |
+ ); |
|
| 1543 |
+ } |
|
| 1544 |
+ |
|
| 1545 |
+ #[test] |
|
| 1546 |
+ fn stripping_extensions_preserves_nested_apps_and_frameworks() {
|
|
| 1547 |
+ let temp = tempfile::tempdir().unwrap(); |
|
| 1548 |
+ let app_path = temp.path().join("Payload").join("Test.app");
|
|
| 1549 |
+ let extension_path = app_path.join("PlugIns").join("Widget.appex");
|
|
| 1550 |
+ let nested_app_path = app_path.join("Watch").join("WatchApp.app");
|
|
| 1551 |
+ let nested_extension_path = nested_app_path.join("PlugIns").join("Companion.appex");
|
|
| 1552 |
+ let framework_path = app_path.join("Frameworks").join("Example.framework");
|
|
| 1553 |
+ for path in [ |
|
| 1554 |
+ &extension_path, |
|
| 1555 |
+ &nested_app_path, |
|
| 1556 |
+ &nested_extension_path, |
|
| 1557 |
+ &framework_path, |
|
| 1558 |
+ ] {
|
|
| 1559 |
+ fs::create_dir_all(path).unwrap(); |
|
| 1560 |
+ fs::write(path.join("content"), b"test").unwrap();
|
|
| 1561 |
+ } |
|
| 1562 |
+ |
|
| 1563 |
+ strip_app_extensions(&app_path).unwrap(); |
|
| 1564 |
+ |
|
| 1565 |
+ assert!(!extension_path.exists()); |
|
| 1566 |
+ assert!(!nested_extension_path.exists()); |
|
| 1567 |
+ assert!(nested_app_path.exists()); |
|
| 1568 |
+ assert!(framework_path.exists()); |
|
| 1493 | 1569 |
} |
| 1494 | 1570 |
|
| 1495 | 1571 |
#[test] |
| ... | ... |
@@ -41,6 +41,13 @@ pub(crate) struct AppMetadata {
|
| 41 | 41 |
pub(crate) struct NestedBundle {
|
| 42 | 42 |
pub(crate) name: String, |
| 43 | 43 |
pub(crate) bundle_id: String, |
| 44 |
+ pub(crate) kind: NestedBundleKind, |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
|
| 48 |
+pub(crate) enum NestedBundleKind {
|
|
| 49 |
+ App, |
|
| 50 |
+ AppExtension, |
|
| 44 | 51 |
} |
| 45 | 52 |
|
| 46 | 53 |
#[derive(Clone, Debug, PartialEq)] |
| ... | ... |
@@ -2883,6 +2883,24 @@ impl SettingsWindow {
|
| 2883 | 2883 |
.detach(); |
| 2884 | 2884 |
} |
| 2885 | 2885 |
|
| 2886 |
+ fn set_strip_extensions( |
|
| 2887 |
+ &mut self, |
|
| 2888 |
+ checked: &bool, |
|
| 2889 |
+ window: &mut Window, |
|
| 2890 |
+ cx: &mut Context<Self>, |
|
| 2891 |
+ ) {
|
|
| 2892 |
+ window.focus(&self.focus_handle, cx); |
|
| 2893 |
+ let Some(mut snapshot) = self.app_settings_snapshot(cx) else {
|
|
| 2894 |
+ return; |
|
| 2895 |
+ }; |
|
| 2896 |
+ if snapshot.app.app_extension_count() == 0 {
|
|
| 2897 |
+ return; |
|
| 2898 |
+ } |
|
| 2899 |
+ snapshot.app.strip_extensions = *checked; |
|
| 2900 |
+ self.replace_app_in_parent(snapshot.app_index, snapshot.app, cx); |
|
| 2901 |
+ cx.notify(); |
|
| 2902 |
+ } |
|
| 2903 |
+ |
|
| 2886 | 2904 |
fn begin_app_detail_edit( |
| 2887 | 2905 |
&mut self, |
| 2888 | 2906 |
field: AppMetadataField, |
| ... | ... |
@@ -3430,6 +3448,7 @@ mod tests {
|
| 3430 | 3448 |
vec![SupportedDeviceFamily::IPhone], |
| 3431 | 3449 |
), |
| 3432 | 3450 |
nested_bundles: Vec::new(), |
| 3451 |
+ strip_extensions: false, |
|
| 3433 | 3452 |
path: format!("/tmp/{name}.ipa"),
|
| 3434 | 3453 |
icon_path: None, |
| 3435 | 3454 |
icon_override_path: None, |
| ... | ... |
@@ -1,5 +1,5 @@ |
| 1 | 1 |
use super::*; |
| 2 |
-use gpui_component::{checkbox::Checkbox, Sizable as _};
|
|
| 2 |
+use gpui_component::{checkbox::Checkbox, Disableable as _, Sizable as _};
|
|
| 3 | 3 |
|
| 4 | 4 |
pub(super) struct AppViewProps<'a> {
|
| 5 | 5 |
pub(super) focus_handle: &'a FocusHandle, |
| ... | ... |
@@ -130,6 +130,7 @@ pub(super) fn render(props: AppViewProps<'_>, cx: &mut Context<SettingsWindow>) |
| 130 | 130 |
.child(read_only_app_detail_row("IPA", app.path.to_string()))
|
| 131 | 131 |
.child(read_only_app_detail_row("Patches", patch_summary)),
|
| 132 | 132 |
) |
| 133 |
+ .child(strip_extensions_checkbox(app, cx)) |
|
| 133 | 134 |
.child(entitlements_table( |
| 134 | 135 |
app, |
| 135 | 136 |
team_id, |
| ... | ... |
@@ -141,6 +142,56 @@ pub(super) fn render(props: AppViewProps<'_>, cx: &mut Context<SettingsWindow>) |
| 141 | 142 |
)) |
| 142 | 143 |
} |
| 143 | 144 |
|
| 145 |
+fn strip_extensions_checkbox( |
|
| 146 |
+ app: &AppOption, |
|
| 147 |
+ cx: &mut Context<SettingsWindow>, |
|
| 148 |
+) -> impl IntoElement {
|
|
| 149 |
+ let extension_count = app.app_extension_count(); |
|
| 150 |
+ let detail = match extension_count {
|
|
| 151 |
+ 0 => "No embedded app extensions were detected in this IPA.".to_string(), |
|
| 152 |
+ 1 => "Remove the embedded app extension before signing. Its App ID and profile will not be required." |
|
| 153 |
+ .to_string(), |
|
| 154 |
+ count => format!( |
|
| 155 |
+ "Remove {count} embedded app extensions before signing. Their App IDs and profiles will not be required."
|
|
| 156 |
+ ), |
|
| 157 |
+ }; |
|
| 158 |
+ |
|
| 159 |
+ Checkbox::new("strip-app-extensions")
|
|
| 160 |
+ .p_3() |
|
| 161 |
+ .rounded_md() |
|
| 162 |
+ .border_1() |
|
| 163 |
+ .border_color(rgb(0xd8e0df)) |
|
| 164 |
+ .bg(rgb(0xffffff)) |
|
| 165 |
+ .hover(|style| style.bg(rgb(0xf6f9f9))) |
|
| 166 |
+ .items_center() |
|
| 167 |
+ .small() |
|
| 168 |
+ .checked(app.strip_extensions) |
|
| 169 |
+ .disabled(extension_count == 0) |
|
| 170 |
+ .on_click(cx.listener(SettingsWindow::set_strip_extensions)) |
|
| 171 |
+ .child( |
|
| 172 |
+ div() |
|
| 173 |
+ .min_w_0() |
|
| 174 |
+ .flex() |
|
| 175 |
+ .flex_1() |
|
| 176 |
+ .flex_col() |
|
| 177 |
+ .gap_0p5() |
|
| 178 |
+ .child( |
|
| 179 |
+ div() |
|
| 180 |
+ .text_sm() |
|
| 181 |
+ .font_weight(FontWeight::SEMIBOLD) |
|
| 182 |
+ .text_color(rgb(0x24333a)) |
|
| 183 |
+ .child("Strip app extensions"),
|
|
| 184 |
+ ) |
|
| 185 |
+ .child( |
|
| 186 |
+ div() |
|
| 187 |
+ .text_xs() |
|
| 188 |
+ .text_color(rgb(0x6a7a81)) |
|
| 189 |
+ .whitespace_normal() |
|
| 190 |
+ .child(detail), |
|
| 191 |
+ ), |
|
| 192 |
+ ) |
|
| 193 |
+} |
|
| 194 |
+ |
|
| 144 | 195 |
fn app_settings_error(error: SharedString) -> impl IntoElement {
|
| 145 | 196 |
div() |
| 146 | 197 |
.rounded_md() |
| 147 | 198 |