1mod assemblers;
7#[cfg(test)]
8mod assembly_test;
9mod bazel_merge;
10mod bazel_prune;
11mod cargo_resource_assign;
12mod cargo_workspace_merge;
13mod composer_resource_assign;
14mod conda_rootfs_merge;
15mod debian_source_merge;
16pub mod file_ref_resolve;
17mod hackage_merge;
18mod huggingface_merge;
19mod nested_merge;
20mod nix_flake_compat_merge;
21mod npm_resource_assign;
22mod npm_workspace_merge;
23mod nuget_cpm_resolve;
24mod python_requirements_assign;
25mod ruby_resource_assign;
26mod sibling_merge;
27mod swift_merge;
28mod topology;
29mod windows_update_merge;
30
31use std::collections::{HashMap, HashSet};
32use std::path::PathBuf;
33use std::sync::LazyLock;
34
35use crate::models::{DatasourceId, FileInfo, Package, PackageUid, TopLevelDependency};
36
37pub use assemblers::ASSEMBLERS;
38
39type DirectoryMergeOutput = (Option<Package>, Vec<TopLevelDependency>, Vec<usize>);
40
41static ASSEMBLER_LOOKUP: LazyLock<HashMap<DatasourceId, DatasourceId>> = LazyLock::new(|| {
44 let mut lookup = HashMap::new();
45 for config in ASSEMBLERS {
46 let key = *config
47 .datasource_ids
48 .first()
49 .expect("assembler must have at least one datasource_id");
50 for &dsid in config.datasource_ids {
51 lookup.insert(dsid, key);
52 }
53 }
54 lookup
55});
56
57static ASSEMBLER_CONFIG_LOOKUP: LazyLock<HashMap<DatasourceId, &'static AssemblerConfig>> =
58 LazyLock::new(|| {
59 let mut lookup = HashMap::new();
60 for config in ASSEMBLERS {
61 let key = *config
62 .datasource_ids
63 .first()
64 .expect("assembler must have at least one datasource_id");
65 lookup.insert(key, config);
66 }
67 lookup
68 });
69
70pub struct AssemblyResult {
73 pub packages: Vec<Package>,
74 pub dependencies: Vec<TopLevelDependency>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum AssemblyMode {
80 SiblingMerge,
82 OnePerPackageData,
85}
86
87pub struct AssemblerConfig {
88 pub datasource_ids: &'static [DatasourceId],
89 pub sibling_file_patterns: &'static [&'static str],
90 pub mode: AssemblyMode,
91}
92
93pub fn assemble(files: &mut [FileInfo]) -> AssemblyResult {
99 let assembler_lookup = &*ASSEMBLER_LOOKUP;
100 let assembler_config_lookup = &*ASSEMBLER_CONFIG_LOOKUP;
101 let mut packages = Vec::new();
102 let mut dependencies = Vec::new();
103
104 let dir_files = group_files_by_directory(files);
105 let topology_plan = topology::TopologyPlan::build(files, &dir_files);
106
107 for file_indices in dir_files.values() {
108 let mut groups: HashSet<DatasourceId> = HashSet::new();
109
110 for &idx in file_indices {
111 for pkg_data in &files[idx].package_data {
112 if let Some(dsid) = pkg_data.datasource_id
113 && let Some(&config_key) = assembler_lookup.get(&dsid)
114 {
115 groups.insert(config_key);
116 }
117 }
118 }
119
120 for &config_key in &groups {
121 let config = assembler_config_lookup
122 .get(&config_key)
123 .copied()
124 .expect("assembler config must exist");
125
126 if topology_plan.claims_directory_assembly(config, file_indices, files) {
127 continue;
128 }
129
130 if let Some(special_merger) = assemblers::special_directory_merger_for(config_key) {
131 let results = special_merger.run(config, files, file_indices);
132 apply_directory_merge_results(files, &mut packages, &mut dependencies, results);
133 continue;
134 }
135
136 match config.mode {
137 AssemblyMode::SiblingMerge => {
138 let results = sibling_merge::assemble_siblings(config, files, file_indices);
139 apply_directory_merge_results(files, &mut packages, &mut dependencies, results);
140 }
141 AssemblyMode::OnePerPackageData => {
142 let results = assemble_one_per_package_data(config, files, file_indices)
143 .into_iter()
144 .map(|(pkg, deps, affected_idx)| (Some(pkg), deps, vec![affected_idx]))
145 .collect();
146 apply_directory_merge_results(files, &mut packages, &mut dependencies, results);
147 }
148 }
149 }
150 }
151
152 topology_plan.apply_directory_scoped_domains(files, &mut packages, &mut dependencies);
153
154 for config in ASSEMBLERS {
155 if config.mode != AssemblyMode::SiblingMerge {
156 continue;
157 }
158 if let Some((pkg, deps, affected_indices)) =
159 nested_merge::assemble_nested_patterns(files, config)
160 {
161 let package_uid = pkg.package_uid.clone();
162 let purl = pkg.purl.clone();
163 let removed_package_uids: Vec<PackageUid> = packages
164 .iter()
165 .filter(|p| p.purl == purl)
166 .map(|p| p.package_uid.clone())
167 .collect();
168
169 packages.retain(|p| p.purl != purl);
170 dependencies.retain(|d| {
171 d.for_package_uid.as_ref() != Some(&package_uid)
172 && !removed_package_uids
173 .iter()
174 .any(|old_uid| d.for_package_uid.as_ref() == Some(old_uid))
175 });
176
177 for idx in &affected_indices {
178 files[*idx].for_packages.clear();
179 files[*idx].for_packages.push(package_uid.clone());
180 }
181
182 packages.push(pkg);
183 dependencies.extend(deps);
184 }
185 }
186
187 assemblers::run_post_assembly_passes(files, &mut packages, &mut dependencies, &topology_plan);
188 hoist_unassembled_file_dependencies(files, &mut dependencies);
189
190 for package in &mut packages {
191 package.datafile_paths.sort();
192 package.datafile_paths.dedup();
193 package.datasource_ids.sort_by_key(|left| left.to_string());
194 package.datasource_ids.dedup();
195 }
196
197 for file in files.iter_mut() {
198 file.for_packages
199 .sort_by(|left, right| left.stable_key().cmp(right.stable_key()));
200 file.for_packages.dedup();
201 }
202
203 packages
204 .sort_by(|left, right| stable_package_sort_key(left).cmp(&stable_package_sort_key(right)));
205 dependencies.sort_by(|left, right| {
206 left.purl
207 .as_deref()
208 .cmp(&right.purl.as_deref())
209 .then_with(|| {
210 left.extracted_requirement
211 .as_deref()
212 .cmp(&right.extracted_requirement.as_deref())
213 })
214 .then_with(|| left.scope.as_deref().cmp(&right.scope.as_deref()))
215 .then_with(|| left.datafile_path.cmp(&right.datafile_path))
216 .then_with(|| {
217 left.datasource_id
218 .to_string()
219 .cmp(&right.datasource_id.to_string())
220 })
221 .then_with(|| {
222 left.for_package_uid
223 .as_ref()
224 .map(|uid| uid.stable_key())
225 .cmp(&right.for_package_uid.as_ref().map(|uid| uid.stable_key()))
226 })
227 });
228
229 AssemblyResult {
230 packages,
231 dependencies,
232 }
233}
234
235fn apply_directory_merge_results(
236 files: &mut [FileInfo],
237 packages: &mut Vec<Package>,
238 dependencies: &mut Vec<TopLevelDependency>,
239 results: Vec<DirectoryMergeOutput>,
240) {
241 for (package, deps, affected_indices) in results {
242 if let Some(package) = package {
243 let package_uid = package.package_uid.clone();
244 for idx in &affected_indices {
245 if !files[*idx].for_packages.contains(&package_uid) {
246 files[*idx].for_packages.push(package_uid.clone());
247 }
248 }
249 packages.push(package);
250 }
251 dependencies.extend(deps);
252 }
253}
254
255fn hoist_unassembled_file_dependencies(
256 files: &[FileInfo],
257 dependencies: &mut Vec<TopLevelDependency>,
258) {
259 for file in files {
260 if !file.for_packages.is_empty() {
261 continue;
262 }
263
264 for pkg_data in &file.package_data {
265 let Some(datasource_id) = pkg_data.datasource_id else {
266 continue;
267 };
268
269 if !should_hoist_unassembled_dependencies(datasource_id) {
270 continue;
271 }
272
273 dependencies.extend(pkg_data.dependencies.iter().map(|dep| {
274 TopLevelDependency::from_dependency(dep, file.path.clone(), datasource_id, None)
275 }));
276 }
277 }
278}
279
280const HOIST_IF_UNOWNED_DATASOURCE_IDS: &[DatasourceId] = &[DatasourceId::PipRequirements];
281
282fn should_hoist_unassembled_dependencies(datasource_id: DatasourceId) -> bool {
283 if HOIST_IF_UNOWNED_DATASOURCE_IDS.contains(&datasource_id) {
284 return true;
285 }
286
287 if !assemblers::UNASSEMBLED_DATASOURCE_IDS.contains(&datasource_id) {
288 return false;
289 }
290
291 !matches!(
292 datasource_id,
293 DatasourceId::NugetDirectoryBuildProps | DatasourceId::NugetDirectoryPackagesProps
294 )
295}
296
297fn stable_package_sort_key(package: &Package) -> (Option<&str>, Option<&str>, Option<&str>, &str) {
298 (
299 package.purl.as_deref(),
300 package.name.as_deref(),
301 package.version.as_deref(),
302 package
303 .datafile_paths
304 .first()
305 .map(String::as_str)
306 .unwrap_or(""),
307 )
308}
309
310fn assemble_one_per_package_data(
311 config: &AssemblerConfig,
312 files: &[FileInfo],
313 file_indices: &[usize],
314) -> Vec<(Package, Vec<TopLevelDependency>, usize)> {
315 let mut results = Vec::new();
316
317 for &idx in file_indices {
318 let file = &files[idx];
319 for pkg_data in &file.package_data {
320 let dsid_matches = pkg_data
321 .datasource_id
322 .is_some_and(|dsid| config.datasource_ids.contains(&dsid));
323
324 if !dsid_matches
325 || pkg_data.purl.is_none()
326 || should_skip_placeholder_only_cocoapods_podspec(pkg_data)
327 {
328 continue;
329 }
330
331 let datafile_path = file.path.clone();
332 let datasource_id = pkg_data.datasource_id.expect("datasource_id must be Some");
333 let pkg = Package::from_package_data(pkg_data, datafile_path.clone());
334 let for_package_uid = Some(pkg.package_uid.clone());
335
336 let deps: Vec<TopLevelDependency> = pkg_data
337 .dependencies
338 .iter()
339 .filter(|dep| dep.purl.is_some() || dep.extracted_requirement.is_some())
340 .map(|dep| {
341 TopLevelDependency::from_dependency(
342 dep,
343 datafile_path.clone(),
344 datasource_id,
345 for_package_uid.clone(),
346 )
347 })
348 .collect();
349
350 results.push((pkg, deps, idx));
351 }
352 }
353
354 results
355}
356
357pub(super) fn should_skip_placeholder_only_cocoapods_podspec(
358 pkg_data: &crate::models::PackageData,
359) -> bool {
360 pkg_data.datasource_id == Some(DatasourceId::CocoapodsPodspec)
361 && pkg_data
362 .extra_data
363 .as_ref()
364 .and_then(|data| data.get("dynamic_identity_placeholders"))
365 .and_then(|value| value.as_bool())
366 == Some(true)
367}
368
369fn group_files_by_directory(files: &[FileInfo]) -> HashMap<PathBuf, Vec<usize>> {
371 let mut groups: HashMap<PathBuf, Vec<usize>> = HashMap::new();
372 for (idx, file) in files.iter().enumerate() {
373 if let Some(parent) = std::path::Path::new(&file.path).parent() {
374 groups.entry(parent.to_path_buf()).or_default().push(idx);
375 }
376 }
377 groups
378}