From 685a87901680a6421680720413b8eefe9b5707c6 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Fri, 12 Sep 2025 08:34:46 -0400 Subject: [PATCH 1/2] Fix: symlinks in normalized upload --- src/commands/build/upload.rs | 50 ++++++++++++++++++++++++++++++++++++ src/utils/build/normalize.rs | 23 +++++++++++++---- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index c47e948df2..70d1ec81bc 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -651,4 +651,54 @@ mod tests { ); Ok(()) } + + #[test] + #[cfg(not(windows))] + fn test_normalize_directory_preserves_symlinks() -> Result<()> { + use std::os::unix::fs::symlink; + + let temp_dir = crate::utils::fs::TempDir::create()?; + let test_dir = temp_dir.path().join("TestApp.xcarchive"); + fs::create_dir_all(test_dir.join("Products"))?; + + // Create a regular file + fs::write(test_dir.join("Products").join("app.txt"), "test content")?; + + // Create a symlink pointing to the regular file + let symlink_path = test_dir.join("Products").join("app_link.txt"); + symlink("app.txt", &symlink_path)?; + + let result_zip = normalize_directory(&test_dir, temp_dir.path())?; + let zip_file = fs::File::open(result_zip.path())?; + let mut archive = ZipArchive::new(zip_file)?; + + // Check that both the regular file and symlink are in the zip + let mut has_regular_file = false; + let mut has_symlink = false; + + for i in 0..archive.len() { + let file = archive.by_index(i)?; + let file_name = file.name(); + + if file_name == "TestApp.xcarchive/Products/app.txt" { + has_regular_file = true; + // Verify it's actually a regular file, not a symlink + assert!( + !file.is_symlink(), + "app.txt should be a regular file, not a symlink" + ); + } else if file_name == "TestApp.xcarchive/Products/app_link.txt" { + has_symlink = true; + // Verify it's actually a symlink + assert!( + file.is_symlink(), + "app_link.txt should be a symlink in the zip" + ); + } + } + + assert!(has_regular_file, "Regular file should be in zip"); + assert!(has_symlink, "Symlink should be preserved in zip"); + Ok(()) + } } diff --git a/src/utils/build/normalize.rs b/src/utils/build/normalize.rs index e06be75ed7..892f9d3ce5 100644 --- a/src/utils/build/normalize.rs +++ b/src/utils/build/normalize.rs @@ -17,10 +17,13 @@ use zip::{DateTime, ZipWriter}; fn sort_entries(path: &Path) -> Result> { Ok(WalkDir::new(path) - .follow_links(true) .into_iter() .filter_map(Result::ok) - .filter(|entry| entry.path().is_file()) + .filter(|entry| { + let path = entry.path(); + // Include both regular files and symlinks + path.is_file() || path.is_symlink() + }) .map(|entry| { let entry_path = entry.into_path(); let relative_path = entry_path.strip_prefix(path)?.to_owned(); @@ -52,9 +55,19 @@ fn add_entries_to_zip( let zip_path = format!("{directory_name}/{}", relative_path.to_string_lossy()); - zip.start_file(zip_path, options)?; - let file_byteview = ByteView::open(&entry_path)?; - zip.write_all(file_byteview.as_slice())?; + if entry_path.is_symlink() { + // Handle symlinks by reading the target path and writing it as a symlink + let target = std::fs::read_link(&entry_path)?; + let target_str = target.to_string_lossy(); + + // Create a symlink entry in the zip + zip.add_symlink(zip_path, &target_str, options)?; + } else { + // Handle regular files + zip.start_file(zip_path, options)?; + let file_byteview = ByteView::open(&entry_path)?; + zip.write_all(file_byteview.as_slice())?; + } file_count += 1; } From b36d7350c06ebd66b6a5e6fcdbd61fc0e545fb86 Mon Sep 17 00:00:00 2001 From: Noah Martin Date: Mon, 15 Sep 2025 12:02:05 -0400 Subject: [PATCH 2/2] PR feedback --- src/commands/build/upload.rs | 4 +--- src/utils/build/normalize.rs | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index 70d1ec81bc..81be6fc1b2 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -570,6 +570,7 @@ fn upload_file( mod tests { use super::*; use std::fs; + use std::os::unix::fs::symlink; use zip::ZipArchive; #[test] @@ -653,10 +654,7 @@ mod tests { } #[test] - #[cfg(not(windows))] fn test_normalize_directory_preserves_symlinks() -> Result<()> { - use std::os::unix::fs::symlink; - let temp_dir = crate::utils::fs::TempDir::create()?; let test_dir = temp_dir.path().join("TestApp.xcarchive"); fs::create_dir_all(test_dir.join("Products"))?; diff --git a/src/utils/build/normalize.rs b/src/utils/build/normalize.rs index 892f9d3ce5..cb08cb2957 100644 --- a/src/utils/build/normalize.rs +++ b/src/utils/build/normalize.rs @@ -75,6 +75,8 @@ fn add_entries_to_zip( } // For XCArchive directories, we'll zip the entire directory +// It's important to not change the contents of the directory or the size +// analysis will be wrong and the code signature will break. pub fn normalize_directory(path: &Path, parsed_assets_path: &Path) -> Result { debug!("Creating normalized zip for directory: {}", path.display());