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