1mod assemblers;
8#[cfg(test)]
9mod assembly_test;
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::{BTreeSet, 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 groups = active_config_keys(files, file_indices, assembler_lookup);
109
110 for &config_key in &groups {
111 let config = assembler_config_lookup
112 .get(&config_key)
113 .copied()
114 .expect("assembler config must exist");
115
116 if topology_plan.claims_directory_assembly(config, file_indices, files) {
117 continue;
118 }
119
120 if let Some(special_merger) = assemblers::special_directory_merger_for(config_key) {
121 let results = special_merger.run(config, files, file_indices);
122 apply_directory_merge_results(files, &mut packages, &mut dependencies, results);
123 continue;
124 }
125
126 match config.mode {
127 AssemblyMode::SiblingMerge => {
128 let results = sibling_merge::assemble_siblings(config, files, file_indices);
129 apply_directory_merge_results(files, &mut packages, &mut dependencies, results);
130 }
131 AssemblyMode::OnePerPackageData => {
132 let results = assemble_one_per_package_data(config, files, file_indices)
133 .into_iter()
134 .map(|(pkg, deps, affected_idx)| (Some(pkg), deps, vec![affected_idx]))
135 .collect();
136 apply_directory_merge_results(files, &mut packages, &mut dependencies, results);
137 }
138 }
139 }
140 }
141
142 topology_plan.apply_directory_scoped_domains(files, &mut packages, &mut dependencies);
143
144 for config in ASSEMBLERS {
145 if config.mode != AssemblyMode::SiblingMerge {
146 continue;
147 }
148 if let Some((pkg, deps, affected_indices)) =
149 nested_merge::assemble_nested_patterns(files, config)
150 {
151 let package_uid = pkg.package_uid.clone();
152
153 let removed_package_uids: HashSet<PackageUid> = affected_indices
159 .iter()
160 .flat_map(|idx| files[*idx].for_packages.iter().cloned())
161 .filter(|uid| *uid != package_uid)
162 .collect();
163
164 packages.retain(|p| !removed_package_uids.contains(&p.package_uid));
165 dependencies.retain(|d| {
166 d.for_package_uid
167 .as_ref()
168 .is_none_or(|old_uid| !removed_package_uids.contains(old_uid))
169 });
170
171 for idx in &affected_indices {
172 files[*idx].for_packages.clear();
173 files[*idx].for_packages.push(package_uid.clone());
174 }
175
176 packages.push(pkg);
177 dependencies.extend(deps);
178 }
179 }
180
181 assemblers::run_post_assembly_passes(files, &mut packages, &mut dependencies, &topology_plan);
182 hoist_unassembled_file_dependencies(files, &mut dependencies);
183
184 for package in &mut packages {
185 package.datafile_paths.sort();
186 package.datafile_paths.dedup();
187 package.datasource_ids.sort_by_key(|left| left.to_string());
188 package.datasource_ids.dedup();
189 }
190
191 for file in files.iter_mut() {
192 file.for_packages
193 .sort_by(|left, right| left.stable_key().cmp(right.stable_key()));
194 file.for_packages.dedup();
195 }
196
197 packages
198 .sort_by(|left, right| stable_package_sort_key(left).cmp(&stable_package_sort_key(right)));
199 dependencies.sort_by(|left, right| {
200 left.purl
201 .as_deref()
202 .cmp(&right.purl.as_deref())
203 .then_with(|| {
204 left.extracted_requirement
205 .as_deref()
206 .cmp(&right.extracted_requirement.as_deref())
207 })
208 .then_with(|| left.scope.as_deref().cmp(&right.scope.as_deref()))
209 .then_with(|| left.datafile_path.cmp(&right.datafile_path))
210 .then_with(|| {
211 left.datasource_id
212 .to_string()
213 .cmp(&right.datasource_id.to_string())
214 })
215 .then_with(|| {
216 left.for_package_uid
217 .as_ref()
218 .map(|uid| uid.stable_key())
219 .cmp(&right.for_package_uid.as_ref().map(|uid| uid.stable_key()))
220 })
221 });
222
223 AssemblyResult {
224 packages,
225 dependencies,
226 }
227}
228
229fn apply_directory_merge_results(
230 files: &mut [FileInfo],
231 packages: &mut Vec<Package>,
232 dependencies: &mut Vec<TopLevelDependency>,
233 results: Vec<DirectoryMergeOutput>,
234) {
235 for (package, deps, affected_indices) in results {
236 if let Some(package) = package {
237 let package_uid = package.package_uid.clone();
238 for idx in &affected_indices {
239 if !files[*idx].for_packages.contains(&package_uid) {
240 files[*idx].for_packages.push(package_uid.clone());
241 }
242 }
243 packages.push(package);
244 }
245 dependencies.extend(deps);
246 }
247}
248
249fn hoist_unassembled_file_dependencies(
250 files: &[FileInfo],
251 dependencies: &mut Vec<TopLevelDependency>,
252) {
253 for file in files {
254 if !file.for_packages.is_empty() {
255 continue;
256 }
257
258 for pkg_data in &file.package_data {
259 let Some(datasource_id) = pkg_data.datasource_id else {
260 continue;
261 };
262
263 if !should_hoist_unassembled_dependencies(datasource_id) {
264 continue;
265 }
266
267 dependencies.extend(pkg_data.dependencies.iter().map(|dep| {
268 TopLevelDependency::from_dependency(dep, file.path.clone(), datasource_id, None)
269 }));
270 }
271 }
272}
273
274const HOIST_IF_UNOWNED_DATASOURCE_IDS: &[DatasourceId] = &[DatasourceId::PipRequirements];
275
276fn should_hoist_unassembled_dependencies(datasource_id: DatasourceId) -> bool {
277 if HOIST_IF_UNOWNED_DATASOURCE_IDS.contains(&datasource_id) {
278 return true;
279 }
280
281 if !assemblers::UNASSEMBLED_DATASOURCE_IDS.contains(&datasource_id) {
282 return false;
283 }
284
285 !matches!(
286 datasource_id,
287 DatasourceId::NugetDirectoryBuildProps | DatasourceId::NugetDirectoryPackagesProps
288 )
289}
290
291fn stable_package_sort_key(package: &Package) -> (Option<&str>, Option<&str>, Option<&str>, &str) {
292 (
293 package.purl.as_deref(),
294 package.name.as_deref(),
295 package.version.as_deref(),
296 package
297 .datafile_paths
298 .first()
299 .map(String::as_str)
300 .unwrap_or(""),
301 )
302}
303
304fn assemble_one_per_package_data(
305 config: &AssemblerConfig,
306 files: &[FileInfo],
307 file_indices: &[usize],
308) -> Vec<(Package, Vec<TopLevelDependency>, usize)> {
309 let mut results = Vec::new();
310
311 for &idx in file_indices {
312 let file = &files[idx];
313 for pkg_data in &file.package_data {
314 let dsid_matches = pkg_data
315 .datasource_id
316 .is_some_and(|dsid| config.datasource_ids.contains(&dsid));
317
318 if !dsid_matches
319 || pkg_data.purl.is_none()
320 || should_skip_placeholder_only_cocoapods_podspec(pkg_data)
321 {
322 continue;
323 }
324
325 let datafile_path = file.path.clone();
326 let datasource_id = pkg_data.datasource_id.expect("datasource_id must be Some");
327 let pkg = Package::from_package_data(pkg_data, datafile_path.clone());
328 let for_package_uid = Some(pkg.package_uid.clone());
329
330 let deps: Vec<TopLevelDependency> = pkg_data
331 .dependencies
332 .iter()
333 .filter(|dep| dep.purl.is_some() || dep.extracted_requirement.is_some())
334 .map(|dep| {
335 TopLevelDependency::from_dependency(
336 dep,
337 datafile_path.clone(),
338 datasource_id,
339 for_package_uid.clone(),
340 )
341 })
342 .collect();
343
344 results.push((pkg, deps, idx));
345 }
346 }
347
348 results
349}
350
351pub(super) fn should_skip_placeholder_only_cocoapods_podspec(
352 pkg_data: &crate::models::PackageData,
353) -> bool {
354 pkg_data.datasource_id == Some(DatasourceId::CocoapodsPodspec)
355 && pkg_data
356 .extra_data
357 .as_ref()
358 .and_then(|data| data.get("dynamic_identity_placeholders"))
359 .and_then(|value| value.as_bool())
360 == Some(true)
361}
362
363fn active_config_keys(
371 files: &[FileInfo],
372 file_indices: &[usize],
373 assembler_lookup: &HashMap<DatasourceId, DatasourceId>,
374) -> BTreeSet<DatasourceId> {
375 let mut groups: BTreeSet<DatasourceId> = BTreeSet::new();
376 for &idx in file_indices {
377 for pkg_data in &files[idx].package_data {
378 if let Some(dsid) = pkg_data.datasource_id
379 && let Some(&config_key) = assembler_lookup.get(&dsid)
380 {
381 groups.insert(config_key);
382 }
383 }
384 }
385 groups
386}
387
388fn group_files_by_directory(files: &[FileInfo]) -> HashMap<PathBuf, Vec<usize>> {
390 let mut groups: HashMap<PathBuf, Vec<usize>> = HashMap::new();
391 for (idx, file) in files.iter().enumerate() {
392 if let Some(parent) = std::path::Path::new(&file.path).parent() {
393 groups.entry(parent.to_path_buf()).or_default().push(idx);
394 }
395 }
396 groups
397}