From 12e46b57d5007d82a96cc8a2aa3041e72398985f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Feb 2026 09:44:15 +0000 Subject: [PATCH 1/4] fix: install staging cache host deps for correct linking checks When a package output inherits from a staging cache, the cache's host dependencies (e.g. zlib, libiconv) were not installed in the host prefix. This caused the linking checks to fail to attribute shared libraries to their providing packages, showing them as "not found" (red/unmarked) instead of properly linked. The root cause: `install_environments` only installed the output's own host dependencies, but the staging cache's host packages (which provided the libraries the build artifacts link against) were absent. Without their conda-meta records and library files in the prefix, `PrefixInfo::from_prefix()` couldn't map DSOs to packages. The fix merges the staging cache's host resolved packages into the output's host environment before installation, ensuring all relevant packages are present for post-processing (linking checks, relinking). Closes #2186 https://claude.ai/code/session_014mGW5MYfcfu8NFWxUJbU1J --- src/render/resolved_dependencies.rs | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/render/resolved_dependencies.rs b/src/render/resolved_dependencies.rs index 24d60291f..0a10b17e8 100644 --- a/src/render/resolved_dependencies.rs +++ b/src/render/resolved_dependencies.rs @@ -1103,6 +1103,11 @@ impl Output { /// Install the environments of the outputs. Assumes that the dependencies /// for the environment have already been resolved. + /// + /// When this output inherits from a staging cache, the cache's host + /// dependencies are merged into the host environment so that their + /// shared libraries and `conda-meta` records are present during + /// post-processing (linking checks, relinking, etc.). pub async fn install_environments( &self, tool_configuration: &Configuration, @@ -1140,6 +1145,58 @@ impl Output { return Ok(()); } + // When inheriting from a staging cache, the cache's host packages + // (e.g. zlib, libiconv) must also be installed in the host prefix. + // Without them the linking checks cannot attribute shared libraries + // to their providing packages and will report false overlinking. + if let Some(cache_deps) = &self.finalized_cache_dependencies { + if let Some(cache_host) = &cache_deps.host { + let mut merged = dependencies.clone(); + + let host = merged.host.get_or_insert_with(|| ResolvedDependencies { + specs: Vec::new(), + resolved: Vec::new(), + }); + + // Add cache host packages that aren't already present + let existing_names: std::collections::HashSet<_> = host + .resolved + .iter() + .map(|r| r.package_record.name.clone()) + .collect(); + + for record in &cache_host.resolved { + if !existing_names.contains(&record.package_record.name) { + host.resolved.push(record.clone()); + // Also add the spec so the package is tracked as explicit + for spec in &cache_host.specs { + if spec.spec().name.as_ref() + == Some(&rattler_conda_types::PackageNameMatcher::Exact( + record.package_record.name.clone(), + )) + { + host.specs.push(spec.clone()); + break; + } + } + } + } + + tracing::info!( + "Merged {} host packages from staging cache into host environment", + cache_host.resolved.len().saturating_sub( + cache_host + .resolved + .iter() + .filter(|r| existing_names.contains(&r.package_record.name)) + .count() + ) + ); + + return install_environments(self, &merged, tool_configuration).await; + } + } + install_environments(self, dependencies, tool_configuration).await } } From 697048c4db3d95dd9b50d0466e05f10bd448fe38 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Feb 2026 10:10:54 +0000 Subject: [PATCH 2/4] fix: cache prefix info in staging metadata instead of reinstalling host packages Instead of reinstalling the staging cache's host packages during install_environments (which is slow), cache the PrefixInfo data (path-to-package and package nature mappings) in the staging cache metadata. When linking checks run, merge this cached info into the current prefix's PrefixInfo so shared libraries from the staging cache's host packages can be correctly attributed. This avoids the performance cost of re-downloading and installing packages while still providing the linking check with the information it needs. https://claude.ai/code/session_014mGW5MYfcfu8NFWxUJbU1J --- py-rattler-build/rust/src/build.rs | 1 + src/build.rs | 6 ++-- src/lib.rs | 1 + src/post_process/checks.rs | 10 +++++- src/post_process/package_nature.rs | 54 ++++++++++++++++++++++++++++- src/render/resolved_dependencies.rs | 52 --------------------------- src/staging.rs | 39 +++++++++++++++++---- src/types/build_output.rs | 8 +++++ 8 files changed, 109 insertions(+), 62 deletions(-) diff --git a/py-rattler-build/rust/src/build.rs b/py-rattler-build/rust/src/build.rs index 78c973ce9..8c725bd4c 100644 --- a/py-rattler-build/rust/src/build.rs +++ b/py-rattler-build/rust/src/build.rs @@ -225,6 +225,7 @@ pub fn build_rendered_variant_py( finalized_sources: None, finalized_cache_dependencies: None, finalized_cache_sources: None, + cached_prefix_info: None, build_summary: Arc::new(Mutex::new(BuildSummary::default())), system_tools: SystemTools::default(), extra_meta: None, diff --git a/src/build.rs b/src/build.rs index 4752ac802..ffb466858 100644 --- a/src/build.rs +++ b/src/build.rs @@ -137,10 +137,12 @@ pub async fn run_build( // This will build or restore staging caches and return their dependencies/sources if inherited let staging_result = output.process_staging_caches(tool_configuration).await?; - // If we inherit from a staging cache, store its dependencies and sources - if let Some((deps, sources)) = staging_result { + // If we inherit from a staging cache, store its dependencies, sources, + // and cached prefix info (for linking checks) + if let Some((deps, sources, cached_prefix_info)) = staging_result { output.finalized_cache_dependencies = Some(deps); output.finalized_cache_sources = Some(sources); + output.cached_prefix_info = Some(cached_prefix_info); } // Fetch sources for this output diff --git a/src/lib.rs b/src/lib.rs index e9cf8baec..ab44116e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -600,6 +600,7 @@ pub async fn get_build_output( finalized_sources: None, finalized_cache_dependencies: None, finalized_cache_sources: None, + cached_prefix_info: None, system_tools: SystemTools::new(), build_summary: Arc::new(Mutex::new(BuildSummary::default())), extra_meta: Some( diff --git a/src/post_process/checks.rs b/src/post_process/checks.rs index 5431fffcf..29124da95 100644 --- a/src/post_process/checks.rs +++ b/src/post_process/checks.rs @@ -528,7 +528,15 @@ pub fn perform_linking_checks( let dynamic_linking = &output.recipe.build().dynamic_linking; let system_libs = find_system_libs(output)?; - let prefix_info = PrefixInfo::from_prefix(output.prefix())?; + let mut prefix_info = PrefixInfo::from_prefix(output.prefix())?; + + // Merge cached prefix info from the staging cache (if any). + // The staging cache's host packages are not physically installed in the + // prefix (their conda-meta records are absent), so we merge the cached + // mappings to allow the linking checks to attribute shared libraries. + if let Some(cached) = &output.cached_prefix_info { + prefix_info.merge_cached(cached); + } let host_dso_packages = host_run_export_dso_packages(output, &prefix_info.package_to_nature); tracing::trace!("Host run_export DSO packages: {host_dso_packages:#?}",); diff --git a/src/post_process/package_nature.rs b/src/post_process/package_nature.rs index f363c7eee..af59011f7 100644 --- a/src/post_process/package_nature.rs +++ b/src/post_process/package_nature.rs @@ -18,10 +18,11 @@ use std::{ hash::Hash, ops::Sub, path::{Path, PathBuf}, + str::FromStr, }; /// The nature of a package -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] pub enum PackageNature { /// Libraries RunExportsLibrary, @@ -213,6 +214,57 @@ impl PrefixInfo { Ok(prefix_info) } + + /// Merge cached prefix info (from a staging cache) into this PrefixInfo. + /// Cached entries are only added if they don't already exist. + pub fn merge_cached(&mut self, cached: &CachedPrefixInfo) { + for (name_str, nature) in &cached.package_to_nature { + if let Ok(name) = PackageName::from_str(name_str) { + self.package_to_nature.entry(name).or_insert(nature.clone()); + } + } + for (path_str, name_str) in &cached.path_to_package { + if let Ok(name) = PackageName::from_str(name_str) { + let path_buf: CaseInsensitivePathBuf = PathBuf::from(path_str).into(); + self.path_to_package.entry(path_buf).or_insert(name); + } + } + } +} + +/// Serializable prefix info for storing in staging cache metadata. +/// Maps file paths to their owning packages and packages to their nature, +/// so that linking checks can attribute libraries without needing the +/// original conda-meta records installed. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct CachedPrefixInfo { + /// Maps file paths (relative to prefix) to package names + pub path_to_package: HashMap, + /// Maps package names to their nature + pub package_to_nature: HashMap, +} + +impl CachedPrefixInfo { + /// Build a CachedPrefixInfo from a PrefixInfo + pub(crate) fn from_prefix_info(info: &PrefixInfo) -> Self { + Self { + path_to_package: info + .path_to_package + .iter() + .map(|(path, name)| { + ( + path.path.to_string_lossy().to_string(), + name.as_normalized().to_string(), + ) + }) + .collect(), + package_to_nature: info + .package_to_nature + .iter() + .map(|(name, nature)| (name.as_normalized().to_string(), nature.clone())) + .collect(), + } + } } #[cfg(test)] diff --git a/src/render/resolved_dependencies.rs b/src/render/resolved_dependencies.rs index 0a10b17e8..c46851dcf 100644 --- a/src/render/resolved_dependencies.rs +++ b/src/render/resolved_dependencies.rs @@ -1145,58 +1145,6 @@ impl Output { return Ok(()); } - // When inheriting from a staging cache, the cache's host packages - // (e.g. zlib, libiconv) must also be installed in the host prefix. - // Without them the linking checks cannot attribute shared libraries - // to their providing packages and will report false overlinking. - if let Some(cache_deps) = &self.finalized_cache_dependencies { - if let Some(cache_host) = &cache_deps.host { - let mut merged = dependencies.clone(); - - let host = merged.host.get_or_insert_with(|| ResolvedDependencies { - specs: Vec::new(), - resolved: Vec::new(), - }); - - // Add cache host packages that aren't already present - let existing_names: std::collections::HashSet<_> = host - .resolved - .iter() - .map(|r| r.package_record.name.clone()) - .collect(); - - for record in &cache_host.resolved { - if !existing_names.contains(&record.package_record.name) { - host.resolved.push(record.clone()); - // Also add the spec so the package is tracked as explicit - for spec in &cache_host.specs { - if spec.spec().name.as_ref() - == Some(&rattler_conda_types::PackageNameMatcher::Exact( - record.package_record.name.clone(), - )) - { - host.specs.push(spec.clone()); - break; - } - } - } - } - - tracing::info!( - "Merged {} host packages from staging cache into host environment", - cache_host.resolved.len().saturating_sub( - cache_host - .resolved - .iter() - .filter(|r| existing_names.contains(&r.package_record.name)) - .count() - ) - ); - - return install_environments(self, &merged, tool_configuration).await; - } - } - install_environments(self, dependencies, tool_configuration).await } } diff --git a/src/staging.rs b/src/staging.rs index c4a36408b..e64c5c5fb 100644 --- a/src/staging.rs +++ b/src/staging.rs @@ -23,6 +23,7 @@ use crate::{ env_vars, metadata::{Output, build_reindexed_channels}, packaging::Files, + post_process::package_nature::{CachedPrefixInfo, PrefixInfo}, render::resolved_dependencies::{ FinalizedDependencies, RunExportsDownload, install_environments, resolve_dependencies, }, @@ -72,6 +73,13 @@ pub struct StagingCacheMetadata { /// The variant configuration that was used pub variant: BTreeMap, + + /// Cached prefix info (path-to-package and package nature mappings) + /// from the host environment at staging cache build time. + /// This allows linking checks to attribute shared libraries to their + /// providing packages without needing the conda-meta records installed. + #[serde(default)] + pub cached_prefix_info: CachedPrefixInfo, } impl Output { @@ -142,7 +150,7 @@ impl Output { /// 2. If yes, restore the cached files to the prefix /// 3. If no, build the staging cache and save it /// - /// Returns the finalized dependencies and sources from the staging cache + /// Returns the finalized dependencies, sources, and cached prefix info pub async fn build_or_restore_staging_cache( &self, staging: &StagingCache, @@ -151,6 +159,7 @@ impl Output { ( FinalizedDependencies, Vec, + CachedPrefixInfo, ), miette::Error, > { @@ -217,6 +226,7 @@ impl Output { ( FinalizedDependencies, Vec, + CachedPrefixInfo, ), miette::Error, > { @@ -368,6 +378,12 @@ impl Output { .run() .into_diagnostic()?; + // Capture prefix info (path-to-package and package nature mappings) + // from the host environment while conda-meta records are still present. + // This data is needed by linking checks in inheriting outputs. + let prefix_info = PrefixInfo::from_prefix(self.prefix()).into_diagnostic()?; + let cached_prefix_info = CachedPrefixInfo::from_prefix_info(&prefix_info); + // Save metadata let metadata = StagingCacheMetadata { name: staging.name.clone(), @@ -377,6 +393,7 @@ impl Output { work_dir_files: copied_work_dir.copied_paths().to_vec(), prefix: self.prefix().to_path_buf(), variant: staging.used_variant.clone(), + cached_prefix_info, }; let metadata_json = serde_json::to_string_pretty(&metadata).into_diagnostic()?; @@ -388,7 +405,11 @@ impl Output { metadata.work_dir_files.len() ); - Ok((finalized_dependencies, finalized_sources)) + Ok(( + finalized_dependencies, + finalized_sources, + metadata.cached_prefix_info, + )) } /// Restore a staging cache from disk @@ -400,6 +421,7 @@ impl Output { ( FinalizedDependencies, Vec, + CachedPrefixInfo, ), miette::Error, > { @@ -439,7 +461,11 @@ impl Output { metadata.name ); - Ok((metadata.finalized_dependencies, metadata.finalized_sources)) + Ok(( + metadata.finalized_dependencies, + metadata.finalized_sources, + metadata.cached_prefix_info, + )) } /// Process all staging caches for this output @@ -454,6 +480,7 @@ impl Output { Option<( FinalizedDependencies, Vec, + CachedPrefixInfo, )>, miette::Error, > { @@ -470,7 +497,7 @@ impl Output { "Building or restoring staging cache: {}", staging_cache.name ); - let (_deps, _sources) = self + let (_deps, _sources, _prefix_info) = self .build_or_restore_staging_cache(staging_cache, tool_configuration) .await?; } @@ -491,11 +518,11 @@ impl Output { })?; // Get or build the cache - let (deps, sources) = self + let (deps, sources, cached_prefix_info) = self .build_or_restore_staging_cache(staging, tool_configuration) .await?; - Ok(Some((deps, sources))) + Ok(Some((deps, sources, cached_prefix_info))) } else { Ok(None) } diff --git a/src/types/build_output.rs b/src/types/build_output.rs index f00b4762c..d3e50154d 100644 --- a/src/types/build_output.rs +++ b/src/types/build_output.rs @@ -20,6 +20,7 @@ use std::{ use crate::{ console_utils::github_integration_enabled, + post_process::package_nature::CachedPrefixInfo, render::resolved_dependencies::FinalizedDependencies, system_tools::SystemTools, types::{BuildConfiguration, BuildSummary, PlatformWithVirtualPackages}, @@ -50,6 +51,13 @@ pub struct BuildOutput { #[serde(skip_serializing_if = "Option::is_none")] pub finalized_cache_sources: Option>, + /// Cached prefix info from the staging cache's host environment. + /// Used by linking checks to attribute shared libraries to packages + /// that were installed during the staging cache build but are not + /// physically present in the current host prefix. + #[serde(skip_serializing_if = "Option::is_none")] + pub cached_prefix_info: Option, + /// Summary of the build #[serde(skip)] pub build_summary: Arc>, From 00f8fe0bdd48e16fe32c93c008b11c806ecaaaf8 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 25 Feb 2026 13:08:30 +0100 Subject: [PATCH 3/4] more fixes --- src/post_process/checks.rs | 48 ++++++++++++++++++++++++++++++ src/post_process/package_nature.rs | 37 +++++++++++++++++++++-- src/staging.rs | 3 +- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/post_process/checks.rs b/src/post_process/checks.rs index 29124da95..38bfce36e 100644 --- a/src/post_process/checks.rs +++ b/src/post_process/checks.rs @@ -597,6 +597,27 @@ pub fn perform_linking_checks( file_dsos.push((libpath.to_path_buf(), package.clone())); } } + // Fallback: if the library wasn't resolved on disk (e.g. host deps + // not physically installed due to staging cache optimization), try + // to match by relative path against the cached prefix info. + else if resolved.is_none() { + // Try stripping common prefixes like @rpath/ + let rel_path = lib + .strip_prefix("@rpath") + .or_else(|_| lib.strip_prefix("@loader_path")) + .unwrap_or(lib); + // Search cached prefix info for a matching path + if let Some(package) = prefix_info + .find_package_by_filename(rel_path) + && let Some(nature) = + prefix_info.package_to_nature.get(&package) + && nature.provides_shared_objects() + { + // Use the original unresolved path as key so it matches + // the entry in shared_libraries + file_dsos.push((lib.to_path_buf(), package)); + } + } } Some(PackageFile { @@ -677,6 +698,23 @@ pub fn perform_linking_checks( continue; } + // Check if the library is a build artifact from the staging cache + // (i.e. it will be packaged by a sibling output). + if let Some(cached) = &output.cached_prefix_info { + let lib_str = lib.to_string_lossy(); + if cached + .staging_prefix_files + .iter() + .any(|f| f == lib_str.as_ref() || Path::new(f) == lib) + { + link_info.linked_packages.push(LinkedPackage { + name: lib.to_path_buf(), + link_origin: LinkOrigin::PackageItself, + }); + continue; + } + } + // Check if we allow overlinking. if dynamic_linking.missing_dso_allowlist.is_match(lib) { tracing::info!( @@ -720,6 +758,16 @@ pub fn perform_linking_checks( // If there are any host packages with DSOs that we didn't link against, // it is "overdepending". for host_package in host_dso_packages.iter() { + // Skip overdepending check for packages inherited from the staging cache. + // These are transitive dependencies needed by the staging cache's build + // artifacts (sibling outputs) and are expected to not be directly linked + // by this output's files. + if let Some(cached) = &output.cached_prefix_info { + if cached.package_to_nature.contains_key(host_package) { + continue; + } + } + if !package_files .iter() .map(|package| { diff --git a/src/post_process/package_nature.rs b/src/post_process/package_nature.rs index af59011f7..374de10f3 100644 --- a/src/post_process/package_nature.rs +++ b/src/post_process/package_nature.rs @@ -215,6 +215,26 @@ impl PrefixInfo { Ok(prefix_info) } + /// Find a package that provides a file matching the given relative path + /// (e.g., `libz.1.dylib`). Searches path_to_package entries by filename. + pub fn find_package_by_filename(&self, rel_path: &Path) -> Option { + // First try direct lookup with common lib/ prefix + let with_lib_prefix: CaseInsensitivePathBuf = Path::new("lib").join(rel_path).into(); + if let Some(package) = self.path_to_package.get(&with_lib_prefix) { + return Some(package.clone()); + } + + // Fallback: search by filename + if let Some(filename) = rel_path.file_name() { + for (path, package) in &self.path_to_package { + if path.path.file_name() == Some(filename) { + return Some(package.clone()); + } + } + } + None + } + /// Merge cached prefix info (from a staging cache) into this PrefixInfo. /// Cached entries are only added if they don't already exist. pub fn merge_cached(&mut self, cached: &CachedPrefixInfo) { @@ -242,11 +262,20 @@ pub struct CachedPrefixInfo { pub path_to_package: HashMap, /// Maps package names to their nature pub package_to_nature: HashMap, + /// Files produced by the staging cache build script (relative to prefix). + /// These are build artifacts that will be split across sibling outputs. + /// Used by linking checks to recognize libraries from sibling outputs. + #[serde(default)] + pub staging_prefix_files: Vec, } impl CachedPrefixInfo { - /// Build a CachedPrefixInfo from a PrefixInfo - pub(crate) fn from_prefix_info(info: &PrefixInfo) -> Self { + /// Build a CachedPrefixInfo from a PrefixInfo and the staging cache's + /// prefix files (build artifacts). + pub(crate) fn from_prefix_info( + info: &PrefixInfo, + staging_prefix_files: &[PathBuf], + ) -> Self { Self { path_to_package: info .path_to_package @@ -263,6 +292,10 @@ impl CachedPrefixInfo { .iter() .map(|(name, nature)| (name.as_normalized().to_string(), nature.clone())) .collect(), + staging_prefix_files: staging_prefix_files + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(), } } } diff --git a/src/staging.rs b/src/staging.rs index e64c5c5fb..5819620ee 100644 --- a/src/staging.rs +++ b/src/staging.rs @@ -382,7 +382,8 @@ impl Output { // from the host environment while conda-meta records are still present. // This data is needed by linking checks in inheriting outputs. let prefix_info = PrefixInfo::from_prefix(self.prefix()).into_diagnostic()?; - let cached_prefix_info = CachedPrefixInfo::from_prefix_info(&prefix_info); + let cached_prefix_info = + CachedPrefixInfo::from_prefix_info(&prefix_info, &copied_files); // Save metadata let metadata = StagingCacheMetadata { From 6dbc8e0831054bfd1f84de45f13c4971226537ad Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 25 Feb 2026 13:57:06 +0100 Subject: [PATCH 4/4] tiny improvement but still not perfect --- src/post_process/checks.rs | 84 ++++++++++++++++++------------ src/post_process/package_nature.rs | 25 +-------- src/staging.rs | 3 +- 3 files changed, 53 insertions(+), 59 deletions(-) diff --git a/src/post_process/checks.rs b/src/post_process/checks.rs index 38bfce36e..465881060 100644 --- a/src/post_process/checks.rs +++ b/src/post_process/checks.rs @@ -6,7 +6,10 @@ use std::{ path::{Path, PathBuf}, }; -use crate::post_process::{package_nature::PackageNature, relink}; +use crate::post_process::{ + package_nature::{CaseInsensitivePathBuf, PackageNature}, + relink, +}; use crate::{ metadata::Output, post_process::{package_nature::PrefixInfo, relink::RelinkError}, @@ -531,9 +534,9 @@ pub fn perform_linking_checks( let mut prefix_info = PrefixInfo::from_prefix(output.prefix())?; // Merge cached prefix info from the staging cache (if any). - // The staging cache's host packages are not physically installed in the - // prefix (their conda-meta records are absent), so we merge the cached - // mappings to allow the linking checks to attribute shared libraries. + // The staging cache's host packages may not be physically installed in + // the prefix, but we need their path-to-package and nature mappings + // for linking checks to attribute shared libraries correctly. if let Some(cached) = &output.cached_prefix_info { prefix_info.merge_cached(cached); } @@ -584,39 +587,54 @@ pub fn perform_linking_checks( continue; } - let lib = resolved.as_ref().unwrap_or(lib); - if let Ok(libpath) = lib.strip_prefix(host_prefix) - && let Some(package) = prefix_info - .path_to_package - .get(&libpath.to_path_buf().into()) - && let Some(nature) = prefix_info.package_to_nature.get(package) - { - // Accept any package that provides shared objects (DSO libraries, - // interpreters like python providing python3XX.dll, plugin libraries, etc.) - if nature.provides_shared_objects() { - file_dsos.push((libpath.to_path_buf(), package.clone())); + let effective_lib = resolved.as_ref().unwrap_or(lib); + + // Try to attribute the library to a host package. + // First attempt: resolved path stripped of host prefix. + let attributed = + if let Ok(libpath) = effective_lib.strip_prefix(host_prefix) { + let key: CaseInsensitivePathBuf = libpath.to_path_buf().into(); + if let Some(package) = prefix_info.path_to_package.get(&key) + && let Some(nature) = prefix_info.package_to_nature.get(package) + && nature.provides_shared_objects() + { + Some((libpath.to_path_buf(), package.clone())) + } else { + None + } + } else { + None + }; + + // Second attempt: for unresolved @rpath/@loader_path libraries + // (host dep files may not be on disk due to staging cache + // optimization), resolve virtually against path_to_package. + let attributed = attributed.or_else(|| { + if resolved.is_some() { + return None; } - } - // Fallback: if the library wasn't resolved on disk (e.g. host deps - // not physically installed due to staging cache optimization), try - // to match by relative path against the cached prefix info. - else if resolved.is_none() { - // Try stripping common prefixes like @rpath/ - let rel_path = lib + let rel = lib .strip_prefix("@rpath") .or_else(|_| lib.strip_prefix("@loader_path")) - .unwrap_or(lib); - // Search cached prefix info for a matching path - if let Some(package) = prefix_info - .find_package_by_filename(rel_path) - && let Some(nature) = - prefix_info.package_to_nature.get(&package) - && nature.provides_shared_objects() - { - // Use the original unresolved path as key so it matches - // the entry in shared_libraries - file_dsos.push((lib.to_path_buf(), package)); + .ok()?; + // Try common library directories + for dir in &["lib", "bin", "Library/bin"] { + let candidate: CaseInsensitivePathBuf = + Path::new(dir).join(rel).into(); + if let Some(package) = prefix_info.path_to_package.get(&candidate) + && let Some(nature) = prefix_info.package_to_nature.get(package) + && nature.provides_shared_objects() + { + // Use the unresolved path as key so it matches + // the entry in shared_libraries downstream + return Some((lib.to_path_buf(), package.clone())); + } } + None + }); + + if let Some((libpath, package)) = attributed { + file_dsos.push((libpath, package)); } } diff --git a/src/post_process/package_nature.rs b/src/post_process/package_nature.rs index 374de10f3..04a343ac3 100644 --- a/src/post_process/package_nature.rs +++ b/src/post_process/package_nature.rs @@ -215,26 +215,6 @@ impl PrefixInfo { Ok(prefix_info) } - /// Find a package that provides a file matching the given relative path - /// (e.g., `libz.1.dylib`). Searches path_to_package entries by filename. - pub fn find_package_by_filename(&self, rel_path: &Path) -> Option { - // First try direct lookup with common lib/ prefix - let with_lib_prefix: CaseInsensitivePathBuf = Path::new("lib").join(rel_path).into(); - if let Some(package) = self.path_to_package.get(&with_lib_prefix) { - return Some(package.clone()); - } - - // Fallback: search by filename - if let Some(filename) = rel_path.file_name() { - for (path, package) in &self.path_to_package { - if path.path.file_name() == Some(filename) { - return Some(package.clone()); - } - } - } - None - } - /// Merge cached prefix info (from a staging cache) into this PrefixInfo. /// Cached entries are only added if they don't already exist. pub fn merge_cached(&mut self, cached: &CachedPrefixInfo) { @@ -272,10 +252,7 @@ pub struct CachedPrefixInfo { impl CachedPrefixInfo { /// Build a CachedPrefixInfo from a PrefixInfo and the staging cache's /// prefix files (build artifacts). - pub(crate) fn from_prefix_info( - info: &PrefixInfo, - staging_prefix_files: &[PathBuf], - ) -> Self { + pub(crate) fn from_prefix_info(info: &PrefixInfo, staging_prefix_files: &[PathBuf]) -> Self { Self { path_to_package: info .path_to_package diff --git a/src/staging.rs b/src/staging.rs index 5819620ee..047175b82 100644 --- a/src/staging.rs +++ b/src/staging.rs @@ -382,8 +382,7 @@ impl Output { // from the host environment while conda-meta records are still present. // This data is needed by linking checks in inheriting outputs. let prefix_info = PrefixInfo::from_prefix(self.prefix()).into_diagnostic()?; - let cached_prefix_info = - CachedPrefixInfo::from_prefix_info(&prefix_info, &copied_files); + let cached_prefix_info = CachedPrefixInfo::from_prefix_info(&prefix_info, &copied_files); // Save metadata let metadata = StagingCacheMetadata {