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