From 28b926bb12e016428bad4d2a92f216714184bb17 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Tue, 10 Mar 2026 16:25:44 +0000 Subject: [PATCH] fix: normalize archive entry names and manifest paths to forward slashes On Windows, PathBuf joins use backslashes, which leaked into tar archive entry names and the manifest import_paths field. This caused test failures on windows-latest CI and would break cross-platform package extraction. Reuse the existing normalize_path() helper for all archive entry names (app files, module files) and manifest import_path strings so they always use forward slashes regardless of the host OS. --- crates/tower-package/src/lib.rs | 17 ++++++++---- crates/tower-package/tests/package_test.rs | 30 ++++++---------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/crates/tower-package/src/lib.rs b/crates/tower-package/src/lib.rs index 04c7d007..25c8a9a9 100644 --- a/crates/tower-package/src/lib.rs +++ b/crates/tower-package/src/lib.rs @@ -272,12 +272,15 @@ impl Package { for (physical_path, logical_path) in file_paths { // All of the app code goes into the "app" directory. let logical_path = app_dir.join(logical_path); + // Normalize to forward slashes so archive entry names are POSIX-compatible + // on all platforms (Windows PathBuf uses backslashes). + let archive_name = normalize_path(&logical_path)?; let hash = compute_sha256_file(&physical_path).await?; - path_hashes.insert(logical_path.clone(), hash); + path_hashes.insert(PathBuf::from(&archive_name), hash); builder - .append_path_with_name(physical_path, logical_path) + .append_path_with_name(physical_path, &archive_name) .await?; } @@ -301,7 +304,8 @@ impl Package { // The file_name should constitute the logical path let import_path = import_path.file_name().unwrap(); let import_path = module_dir.join(import_path); - let import_path_str = import_path.into_os_string().into_string().unwrap(); + // Normalize to forward slashes for the manifest (POSIX, cross-platform). + let import_path_str = normalize_path(&import_path)?; import_paths.push(import_path_str); // Now we write all of these paths to the modules directory. @@ -310,13 +314,16 @@ impl Package { Ok(p) => module_dir.join(p), Err(_) => continue, }; + // Normalize to forward slashes so archive entry names are POSIX-compatible + // on all platforms (Windows PathBuf uses backslashes). + let archive_name = normalize_path(&logical_path)?; let hash = compute_sha256_file(&physical_path).await?; - path_hashes.insert(logical_path.clone(), hash); + path_hashes.insert(PathBuf::from(&archive_name), hash); debug!("adding file {}", logical_path.display()); builder - .append_path_with_name(physical_path, logical_path) + .append_path_with_name(physical_path, &archive_name) .await?; } } diff --git a/crates/tower-package/tests/package_test.rs b/crates/tower-package/tests/package_test.rs index c201e2c5..18aed248 100644 --- a/crates/tower-package/tests/package_test.rs +++ b/crates/tower-package/tests/package_test.rs @@ -15,17 +15,7 @@ use tokio_tar::Archive; use tower_package::{Manifest, Package, PackageSpec, Parameter}; use tower_telemetry::debug; -macro_rules! make_path { - ($($component:expr),+ $(,)?) => { - { - let mut path = PathBuf::new(); - $( - path.push($component); - )+ - &path.to_string_lossy().to_string() - } - }; -} + #[tokio::test] async fn it_creates_package() { @@ -289,12 +279,9 @@ async fn it_packages_import_paths() { .await .expect("Manifest was not valid JSON"); - // NOTE: These paths are joined by the OS so we need to be more specific about the expected - // path. + // Archive paths are always normalized to forward slashes regardless of OS. assert!( - manifest - .import_paths - .contains(make_path!("modules", "shared")), + manifest.import_paths.contains(&"modules/shared".to_string()), "Import paths {:?} did not contain expected path", manifest.import_paths ); @@ -333,18 +320,19 @@ async fn it_packages_import_paths_nested_within_base_dir() { let files = read_package_files(package).await; // Module files should be under modules/shared/..., NOT modules/libs/shared/... + // Archive paths are always normalized to forward slashes regardless of OS. assert!( - files.contains_key(make_path!("modules", "shared", "__init__.py")), + files.contains_key("modules/shared/__init__.py"), "files {:?} was missing modules/shared/__init__.py", files ); assert!( - files.contains_key(make_path!("modules", "shared", "util.py")), + files.contains_key("modules/shared/util.py"), "files {:?} was missing modules/shared/util.py", files ); assert!( - !files.contains_key(make_path!("modules", "libs", "shared", "__init__.py")), + !files.contains_key("modules/libs/shared/__init__.py"), "files {:?} should NOT contain modules/libs/shared/__init__.py", files ); @@ -355,9 +343,7 @@ async fn it_packages_import_paths_nested_within_base_dir() { .expect("Manifest was not valid JSON"); assert!( - manifest - .import_paths - .contains(make_path!("modules", "shared")), + manifest.import_paths.contains(&"modules/shared".to_string()), "Import paths {:?} did not contain expected path modules/shared", manifest.import_paths );