1use anyhow::{format_err, Context, Error};
2use cargo::{
3 core::{
4 manifest::ManifestMetadata, registry::PackageRegistry, resolver::features::CliFeatures,
5 Dependency, EitherManifest, FeatureMap, FeatureValue, Manifest, Package, PackageId,
6 Registry, SourceId, Summary, Target, TargetKind, Workspace,
7 },
8 ops::{self, PackageMessageFormat, PackageOpts, Packages},
9 sources::{
10 source::{MaybePackage, QueryKind, Source},
11 IndexSummary, RegistrySource, SourceConfigMap,
12 },
13 util::{
14 cache_lock::CacheLockMode, interning::InternedString, toml::read_manifest, FileLock,
15 GlobalContext,
16 },
17};
18use cargo_toml_edit::{self, DependencyFilter, FeatureFilter};
19use cfg_expr::targets::get_builtin_target_by_triple;
20use filetime::{set_file_times, FileTime};
21use flate2::read::GzDecoder;
22use glob::Pattern;
23use indexmap::{IndexMap, IndexSet};
24use itertools::Itertools;
25use regex::Regex;
26use semver::Version;
27use tar::Archive;
28use tempfile;
29
30use crate::config::{testing_ignore_debpolv, FilterTarget, TargetType};
31use crate::errors::Result;
32use crate::util::diff_files;
33use cargo::util::Filesystem;
34use std::fs;
35use std::io::{self, Read};
36use std::path::Path;
37use std::{self, ffi::OsStr};
38use std::{
39 collections::{BTreeMap, HashMap, HashSet},
40 str::FromStr,
41};
42
43pub struct CrateInfo {
44 package: Package,
46 manifest: Manifest,
48 crate_file: FileLock,
49 context: GlobalContext,
50 source_id: SourceId,
51 excludes: Vec<String>,
52 includes: Vec<String>,
53}
54
55pub type AutoPatches = IndexMap<String, Vec<u8>>;
56
57pub type CrateDepInfo = BTreeMap<
58 &'static str, (
61 Vec<&'static str>, Vec<Dependency>,
63 ),
64>;
65
66fn fetch_candidates(registry: &mut PackageRegistry, dep: &Dependency) -> Result<Vec<IndexSummary>> {
67 let mut summaries = match registry.query_vec(dep, QueryKind::Exact) {
68 std::task::Poll::Ready(res) => res?,
69 std::task::Poll::Pending => {
70 registry.block_until_ready()?;
71 return fetch_candidates(registry, dep);
72 }
73 };
74 summaries.sort_by(|a, b| b.package_id().partial_cmp(&a.package_id()).unwrap());
75 Ok(summaries)
76}
77
78pub fn invalidate_crates_io_cache() -> Result<()> {
79 let context = GlobalContext::default()?;
80 let _lock = context.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
81 let source_id = SourceId::crates_io_maybe_sparse_http(&context)?;
82 let yanked_whitelist = HashSet::new();
83 let mut r = RegistrySource::remote(source_id, &yanked_whitelist, &context)?;
84 r.invalidate_cache();
85 Ok(())
86}
87
88pub fn crate_name_ver_to_dep(crate_name: &str, version: Option<&str>) -> Result<Dependency> {
89 let context = GlobalContext::default()?;
91 let source_id = SourceId::crates_io_maybe_sparse_http(&context)?;
92 let version = version.and_then(|v| {
93 if v.is_empty() {
94 None
95 } else if v.starts_with(|c: char| c.is_ascii_digit()) {
96 Some(["=", v].concat())
97 } else {
98 Some(v.to_string())
99 }
100 });
101 Dependency::parse(crate_name, version.as_deref(), source_id)
102}
103
104fn ver_req_to_ver(dep: &Dependency) -> Option<Version> {
107 match dep.version_req() {
108 cargo::util::OptVersionReq::Any => None,
111 cargo::util::OptVersionReq::Req(req) if req.comparators.len() == 1 => {
113 let comp = req.comparators.first().unwrap();
114 match comp.op {
115 semver::Op::Exact
117 | semver::Op::GreaterEq
118 | semver::Op::LessEq
119 | semver::Op::Tilde
120 | semver::Op::Caret => {
121 if let Some(minor) = comp.minor {
122 comp.patch.map(|patch| Version {
124 major: comp.major,
125 minor,
126 patch,
127 pre: comp.pre.clone(),
128 build: semver::BuildMetadata::default(),
129 })
130 } else {
131 None
133 }
134 }
135 _ => None,
137 }
138 }
139 cargo::util::OptVersionReq::Req(_) => None,
141 cargo::util::OptVersionReq::Locked(ver, _) => Some(ver.clone()),
143 cargo::util::OptVersionReq::Precise(ver, _) => Some(ver.clone()),
145 }
146}
147
148pub fn show_dep(dep: &Dependency) -> String {
149 format!("{} {}", dep.package_name(), dep.version_req())
150}
151
152impl CrateInfo {
153 pub fn new(crate_name: &str, version: Option<&str>) -> Result<CrateInfo> {
154 CrateInfo::new_with_update(crate_name, version, true)
155 }
156
157 pub fn new_with_local_crate(
158 crate_name: &str,
159 version: Option<&str>,
160 crate_path: &Path,
161 ) -> Result<CrateInfo> {
162 let context = GlobalContext::default()?;
163 let crate_path = crate_path.canonicalize()?;
164 let source_id = SourceId::for_path(&crate_path)?;
165
166 let (package, crate_file) = {
167 let yanked_whitelist = HashSet::new();
168
169 let mut source = source_id.load(&context, &yanked_whitelist)?;
170
171 let package_id = match version {
172 None => {
173 let dep = Dependency::parse(crate_name, None, source_id)?;
174 let mut package_id: Option<PackageId> = None;
175 loop {
176 match source.query(&dep, QueryKind::Exact, &mut |p| {
177 package_id = Some(p.package_id());
178 }) {
179 std::task::Poll::Ready(res) => {
180 res?;
181 break;
182 }
183 std::task::Poll::Pending => {
184 source.block_until_ready()?;
185 }
186 }
187 }
188 package_id.unwrap()
189 }
190 Some(version) => {
191 let version = version.parse::<Version>()?;
192 PackageId::new(crate_name.into(), version, source_id)
193 }
194 };
195
196 let maybe_package = source.download(package_id)?;
197 let package = match maybe_package {
198 MaybePackage::Ready(p) => Ok(p),
199 _ => Err(format_err!(
200 "Failed to 'download' local crate {} from {}",
201 crate_name,
202 crate_path.display()
203 )),
204 }?;
205
206 let crate_file = {
207 let workspace = Workspace::ephemeral(package.clone(), &context, None, true)?;
208
209 let opts = PackageOpts {
210 gctx: &context,
211 verify: false,
212 list: false,
213 check_metadata: true,
214 allow_dirty: true,
215 cli_features: CliFeatures::from_command_line(&[], true, false)?,
216 jobs: None,
217 targets: Vec::new(),
218 to_package: Packages::Default,
219 keep_going: false,
220 reg_or_index: None,
221 dry_run: false,
222 include_lockfile: false,
223 fmt: PackageMessageFormat::Human,
224 };
225 ops::package(&workspace, &opts)?;
226
227 let filename = format!("{}-{}.crate", crate_name, package_id.version());
228 workspace
229 .target_dir()
230 .join("package")
231 .open_rw_exclusive_create(filename, &context, "crate file")?
232 };
233
234 (package, crate_file)
235 };
236
237 let manifest = package.manifest().clone();
238
239 Ok(CrateInfo {
240 package,
241 manifest,
242 crate_file,
243 context,
244 source_id,
245 excludes: vec![],
246 includes: vec![],
247 })
248 }
249
250 pub fn new_with_update(
251 crate_name: &str,
252 version: Option<&str>,
253 update: bool,
254 ) -> Result<CrateInfo> {
255 let dep = crate_name_ver_to_dep(crate_name, version)?;
256 Self::new_from_dependency(&dep, update)
257 }
258
259 pub fn new_from_dependency(dependency: &Dependency, update: bool) -> Result<CrateInfo> {
260 let mut context = GlobalContext::default()?;
261 if !update {
262 context.configure(
265 0,
266 false,
267 None,
268 false,
269 false,
270 true, &context.target_dir()?.map(Filesystem::into_path_unlocked),
272 &[],
273 &[],
274 )?;
275 }
276
277 let source_id = SourceId::crates_io_maybe_sparse_http(&context)?;
278 let registry_name = format!(
279 "{}-{}",
280 source_id.url().host_str().unwrap_or(""),
281 cargo::util::hex::short_hash(&source_id)
282 );
283 let get_package_info = |context: &GlobalContext,
284 possibly_yanked_ver: Option<&Version>|
285 -> Result<_> {
286 let lock = context.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
287 let mut registry =
288 PackageRegistry::new_with_source_config(context, SourceConfigMap::new(context)?)?;
289
290 if let Some(ver) = possibly_yanked_ver {
294 let pkgids = vec![PackageId::new(
295 dependency.package_name(),
296 ver.clone(),
297 dependency.source_id(),
298 )];
299 debcargo_warn!(
300 "Adding {} to yanked whitelist as last resort..",
301 pkgids.first().unwrap()
302 );
303 registry.add_to_yanked_whitelist(pkgids.into_iter());
304 }
305 registry.lock_patches();
306 let summaries = fetch_candidates(&mut registry, dependency)?;
307 drop(lock);
308 let pkgids = summaries.into_iter().map(|s| s.package_id()).collect_vec();
309 let pkgid = pkgids.iter().max().ok_or_else(|| {
310 format_err!(
311 concat!(
312 "Couldn't find any crate matching {}\n",
313 "Try `debcargo update` to update the crates.io index."
314 ),
315 show_dep(dependency)
316 )
317 })?;
318 let pkgset = registry.get(pkgids.as_slice())?;
319 let package = pkgset.get_one(*pkgid)?;
320
321 let manifest = package.manifest();
322 for f in dependency.features() {
323 if !manifest.summary().features().contains_key(f) {
328 debcargo_bail!(
329 "resolve ({} {}) -> ({}) failed to pick up required feature ({})\n\
330 This can happen with very old or yanked crates. Try patching one of \
331 its dependants, to drop or update the offending dependency.",
332 dependency.package_name(),
333 dependency.version_req(),
334 pkgid,
335 f,
336 )
337 }
338 }
339 let filename = format!("{}-{}.crate", pkgid.name(), pkgid.version());
340 let crate_file = context
341 .registry_cache_path()
342 .join(®istry_name)
343 .open_ro_shared(&filename, context, &filename)?;
344 Ok((package.clone(), manifest.clone(), crate_file))
345 };
346 let (package, manifest, crate_file) = get_package_info(&context, None)
349 .or_else(|_| get_package_info(&GlobalContext::default()?, None))
350 .or_else(|err| {
351 let ver = ver_req_to_ver(dependency);
352 if ver.is_some() {
353 get_package_info(&GlobalContext::default()?, ver.as_ref())
354 } else {
355 Err(err)
356 }
357 })?;
358
359 Ok(CrateInfo {
360 package,
361 manifest,
362 crate_file,
363 context,
364 source_id,
365 excludes: vec![],
366 includes: vec![],
367 })
368 }
369
370 pub fn crate_name(&self) -> &'static str {
371 self.package_id().name().as_str()
372 }
373
374 pub fn version(&self) -> &Version {
375 self.package_id().version()
376 }
377
378 pub fn semver(&self) -> String {
379 match *self.package_id().version() {
380 Version {
381 major: 0, minor, ..
382 } => format!("0.{minor}"),
383 Version { major, .. } => format!("{major}"),
384 }
385 }
386
387 pub fn manifest(&self) -> &Manifest {
388 &self.manifest
389 }
390
391 pub fn replace_manifest(&mut self, path: &Path) -> Result<&Self> {
392 if let EitherManifest::Real(v) = read_manifest(path, self.source_id, &self.context)? {
393 self.manifest = v;
394 }
395 Ok(self)
396 }
397
398 pub fn metadata(&self) -> &ManifestMetadata {
399 self.manifest.metadata()
400 }
401
402 pub fn manifest_path(&self) -> &Path {
403 self.package.manifest_path()
404 }
405
406 pub fn rust_version(&self) -> Option<String> {
407 self.manifest.rust_version().map(ToString::to_string)
408 }
409
410 pub fn targets(&self) -> &[Target] {
411 self.manifest.targets()
412 }
413
414 pub fn is_lib(&self) -> bool {
415 let mut lib = false;
416 for target in self.manifest.targets() {
417 match *target.kind() {
418 TargetKind::Lib(_) => {
419 lib = true;
420 break;
421 }
422 _ => continue,
423 }
424 }
425 lib
426 }
427
428 pub fn get_binary_targets(&self) -> Vec<&str> {
429 let mut bins = Vec::new();
430 for target in self.manifest.targets() {
431 match *target.kind() {
432 TargetKind::Bin => {
433 bins.push(target.name());
434 }
435 _ => continue,
436 }
437 }
438 bins.sort_unstable();
439 bins
440 }
441
442 pub fn summary(&self) -> &Summary {
443 self.manifest.summary()
444 }
445
446 pub fn checksum(&self) -> Option<&str> {
447 self.manifest.summary().checksum()
448 }
449
450 pub fn package_id(&self) -> PackageId {
451 self.manifest.summary().package_id()
452 }
453
454 pub fn crate_file(&self) -> &FileLock {
455 &self.crate_file
456 }
457
458 pub fn dependencies(&self) -> &[Dependency] {
459 self.manifest.dependencies()
460 }
461
462 pub fn dev_dependencies(&self) -> Vec<Dependency> {
463 use cargo::core::dependency::DepKind;
464 let mut deps = vec![];
465 for dep in self.dependencies() {
466 if dep.kind() == DepKind::Development {
467 deps.push(dep.clone());
468 }
469 }
470 deps
471 }
472
473 pub fn get_summary_description(&self) -> (Option<String>, Option<String>) {
474 let (summary, description) = if let Some(ref description) = self.metadata().description {
475 let mut description = description
478 .replace("\n\n", "\r")
479 .replace('\n', " ")
480 .replace('\r', "\n")
481 .trim()
482 .to_string();
483 let re = Regex::new(&format!(
485 r"^(?i)({}|This(\s+\w+)?)(\s*,|\s+is|\s+provides)\s+",
486 self.package_id().name()
487 ))
488 .unwrap();
489 description = re.replace(&description, "").to_string();
490 let re = Regex::new(r"^(?i)(a|an|the)\s+").unwrap();
491 description = re.replace(&description, "").to_string();
492 let re =
493 Regex::new(r"^(?i)(rust\s+)?(implementation|library|tool|crate)\s+(of|to|for)\s+")
494 .unwrap();
495 description = re.replace(&description, "").to_string();
496
497 description = {
499 let mut d = description.chars();
500 match d.next() {
501 None => String::new(),
502 Some(f) => f.to_uppercase().chain(d).collect::<String>(),
503 }
504 };
505
506 let p1 = description.find('\n');
508 let p2 = description.find(". ");
509 match p1.into_iter().chain(p2).min() {
510 Some(p) => {
511 let s = description[..p].trim_end_matches('.').to_string();
512 let d = description[p + 1..].trim();
513 if d.is_empty() {
514 (Some(s), None)
515 } else {
516 (Some(s), Some(d.to_string()))
517 }
518 }
519 None => (Some(description.trim_end_matches('.').to_string()), None),
520 }
521 } else {
522 (None, None)
523 };
524
525 (summary, description)
526 }
527
528 pub fn set_includes_excludes(
530 &mut self,
531 excludes: Option<&Vec<String>>,
532 includes: Option<&Vec<String>>,
533 ) {
534 self.excludes = excludes.cloned().unwrap_or_default();
535 self.includes = includes.cloned().unwrap_or_default();
536 }
537
538 pub fn filter_path(&self, path: &Path) -> std::result::Result<bool, String> {
539 let top_level = path
540 .ancestors()
541 .find(|p| p.parent() == Some(Path::new("")))
542 .unwrap()
543 .to_str()
544 .ok_or_else(|| format!("Failed to get top-level element of {path:?}"))?;
545
546 let matches = move |pattern: &String| -> bool {
547 Pattern::new(&format!("{top_level}/{pattern}"))
548 .unwrap()
549 .matches_path(path)
550 };
551
552 if self.excludes.iter().any(matches) {
553 return Ok(true);
554 }
555 let suspicious = match path.extension().map(OsStr::to_ascii_lowercase) {
556 Some(ext) => {
557 ext == "c"
558 || ext == "a"
559 || ext == "s"
560 || ext == "o"
561 || ext == "so"
562 || ext == "cpp"
563 || ext == "cc"
564 || ext == "cxx"
565 }
566 _ => false,
567 };
568
569 if suspicious {
570 if self.includes.iter().any(matches) {
571 debcargo_info!("Suspicious file, on whitelist so ignored: {:?}", path);
572 Ok(false)
573 } else if testing_ignore_debpolv() {
574 debcargo_warn!("Suspicious file, ignoring as per override: {:?}", path);
575 Ok(false)
576 } else {
577 Err(format!(
578 "Suspicious file, should probably be excluded in debcargo.toml: {path:?}"
579 ))
580 }
581 } else {
582 Ok(false)
583 }
584 }
585
586 pub fn extract_crate(&mut self, path: &Path) -> Result<(bool, Option<String>)> {
587 let mut archive = Archive::new(GzDecoder::new(self.crate_file.file()));
588 let tempdir = tempfile::Builder::new()
589 .prefix("debcargo")
590 .tempdir_in(".")
591 .context("Failed to create tempdir for extracting crate")?;
592 let mut source_modified = false;
593 let mut manifest_normalization_patch = None;
594 let mut last_mtime = 0;
595 let mut err = vec![];
596
597 for entry in archive
598 .entries()
599 .context("Failed iterating crate tarball")?
600 {
601 let mut entry = entry.context("Failed iterating crate tarball entry")?;
602 let entry_path = entry
603 .path()
604 .with_context(|| format!("Failed to convert entry path {:?}", entry.path_bytes()))?
605 .into_owned();
606 match self.filter_path(&entry_path) {
607 Err(e) => err.push(e),
608 Ok(r) => {
609 if r {
610 source_modified = true;
611 continue;
612 }
613 }
614 }
615
616 if !entry.unpack_in(tempdir.path()).with_context(|| {
617 format!("Failed to unpack {entry_path:?} entry of crate tarball")
618 })? {
619 debcargo_bail!("Crate contained path traversals via '..'");
620 }
621
622 if let Ok(mtime) = entry.header().mtime() {
623 if mtime > last_mtime {
624 last_mtime = mtime;
625 }
626 }
627 }
628 if !err.is_empty() {
629 for e in err {
630 debcargo_warn!("{}", e);
631 }
632 debcargo_bail!(
633 "Suspicious files detected, aborting. Ask on #debian-rust if you are stuck."
634 )
635 }
636
637 let entries = tempdir
638 .path()
639 .read_dir()
640 .context("Failed reading extracted tarball tempdir")?
641 .collect::<io::Result<Vec<_>>>()
642 .context("Failed collecting extracted tarball tempdir contents")?;
643 if entries.len() != 1
644 || !entries[0]
645 .file_type()
646 .context(
647 "Failed to determine file type of first dir entry of extracted tarball tempdir",
648 )?
649 .is_dir()
650 {
651 let pkgid = self.package_id();
652 debcargo_bail!(
653 "{}-{}.crate did not unpack to a single top-level directory",
654 pkgid.name(),
655 pkgid.version()
656 );
657 }
658
659 if let Err(e) = fs::rename(entries[0].path(), path) {
660 return Err(Error::from(e).context(format!(
661 concat!(
662 "Could not create source directory {0}\n",
663 "To regenerate, move or remove {0}"
664 ),
665 path.display()
666 )));
667 }
668
669 let _ = fs::remove_dir_all(path.join("debian"));
671
672 let toml_path = path
675 .join("Cargo.toml")
676 .canonicalize()
677 .context("Failed to canonicalize Cargo.toml path in temporary crate unpack dir")?;
678 let mut actual_toml = String::new();
679 fs::File::open(&toml_path)
680 .context("Failed to open crate manifest in extracted crate tarball tempdir")?
681 .read_to_string(&mut actual_toml)
682 .context("Failed to read crate manifest in extracted crate tarball tempdir")?;
683
684 if !actual_toml.contains("AUTOMATICALLY GENERATED BY CARGO") {
685 log::debug!("Cargo.toml not canonicalized..");
690 let orig_toml = actual_toml.clone();
691
692 let mut check_reference = |name, value: Option<&str>| {
696 if let Some(referenced_path) = value {
697 let full_referenced_path = path.join(referenced_path);
698 if !full_referenced_path.exists() {
699 debcargo_warn!(
700 "Cargo.toml references non-existing license_file path: {:?}",
701 full_referenced_path
702 );
703 if let Some((_prefix, file)) = referenced_path.rsplit_once('/') {
704 if path.join(file).exists() {
705 debcargo_info!("Replacing reference with '{}'", file);
706 actual_toml = actual_toml.replace(referenced_path, file);
707 } else {
708 debcargo_info!("Removing reference");
709 actual_toml = actual_toml
710 .replace(&format!("{name} = \"{referenced_path}\""), "");
711 }
712 } else {
713 debcargo_info!("Removing reference");
714 actual_toml =
715 actual_toml.replace(&format!("{name} = \"{referenced_path}\""), "");
716 }
717 return true;
718 }
719 }
720
721 false
722 };
723
724 if check_reference("license-file", self.metadata().license_file.as_deref())
725 || check_reference("readme", self.metadata().readme.as_deref())
726 {
727 fs::write(&toml_path, actual_toml.as_bytes())
728 .context("Failed to overwrite Cargo.toml during normalization")?;
729 }
730 let updated = self
731 .replace_manifest(&toml_path)
732 .context("Failed to reload manifest after canonicalization")?;
733
734 let ws = Workspace::new(&toml_path, &updated.context)
735 .context("Failed to instantiate workspace after reloading manifest")?;
736 let opts = PackageOpts {
737 gctx: &updated.context,
738 list: false,
739 check_metadata: false,
740 allow_dirty: true,
741 verify: false,
742 jobs: None,
743 keep_going: false,
744 to_package: Packages::Default,
745 targets: Vec::new(),
746 cli_features: CliFeatures::new_all(true),
747 reg_or_index: None,
748 dry_run: false,
749 include_lockfile: false,
750 fmt: PackageMessageFormat::Human,
751 };
752 let files = ops::package(&ws, &opts)
753 .context("Failed to run `cargo package` for normalization")?;
754 let file = files
755 .first()
756 .ok_or_else(|| format_err!("No canonicalized archives found.."))?;
757 log::trace!("canonical crate tarball: {file:?}");
758 let file = file.path();
759 let file = fs::File::open(file)?;
760 log::trace!("{file:?}");
761 let mut archive = Archive::new(GzDecoder::new(file));
762
763 for entry in archive.entries()? {
764 let mut entry =
765 entry.context("Failed to parse tar entry in normalized crate tarball")?;
766 let entry_path = entry.path().with_context(|| {
767 format!(
768 "Failed to convert crate tarball entry path to UTF8: {:?}",
769 entry.path_bytes(),
770 )
771 })?;
772 log::trace!("normalized .crate archive entry: {entry_path:?}");
773 let mut components = entry_path.iter();
774 if components.clone().count() == 2
775 && components.next_back() == Some(OsStr::new("Cargo.toml"))
776 {
777 entry
778 .unpack(&toml_path)
779 .context("Failed to extract normalized Cargo.toml")?;
780 break;
781 }
782 }
783
784 log::debug!("generating Cargo.toml normalization patch");
785 fs::rename(path.join("Cargo.toml"), path.join("Cargo.toml.normalized"))
786 .context("Faield to rename normalized Cargo.toml")?;
787 fs::write(path.join("Cargo.toml"), orig_toml.as_bytes())
788 .context("Failed to write original Cargo.toml contents")?;
789 let diff = diff_files(
790 "Cargo.toml",
791 &path.join("Cargo.toml"),
792 &path.join("Cargo.toml.normalized"),
793 )?;
794 manifest_normalization_patch = Some({
795 let mut patch = String::from(
796 "This patch was autogenerated using debcargo. Do not edit manually!\n",
797 );
798 patch.push_str(&diff);
799 patch
800 });
801 fs::remove_file(path.join("Cargo.toml.normalized"))
802 .context("Failed to remove temporary normalized Cargo.toml")?;
803
804 let last_mtime = FileTime::from_unix_time(i64::try_from(last_mtime)?, 0);
808 set_file_times(toml_path, last_mtime, last_mtime)?;
809 debcargo_info!("Cargo.toml manually canonicalized!");
810 }
811 Ok((source_modified, manifest_normalization_patch))
812 }
813}
814
815pub fn all_dependencies_and_features(manifest: &Manifest) -> CrateDepInfo {
818 all_dependencies_and_features_filtered(manifest, false)
819}
820
821fn check_cycle_in_feature<'a>(feature_map: &'a FeatureMap, feature: &'a str) -> bool {
822 fn check_cycle_in_feature_impl<'a>(
823 feature_map: &'a FeatureMap,
824 current: &'a str,
825 processing: &mut Vec<&'a str>,
826 ) -> bool {
827 if processing.contains(¤t) {
828 return true;
829 }
830
831 processing.push(current);
832
833 if let Some(deps) = feature_map.get(current) {
834 for dep in deps {
835 if let FeatureValue::Feature(dep_feature) = dep {
836 if check_cycle_in_feature_impl(feature_map, dep_feature, processing) {
837 return true;
838 }
839 }
840 }
841 }
842
843 processing.pop();
844 false
845 }
846
847 let mut processing = Vec::new();
848 check_cycle_in_feature_impl(feature_map, feature, &mut processing)
849}
850
851pub fn all_dependencies_and_features_filtered(
855 manifest: &Manifest,
856 include_dev_dependencies: bool,
857) -> CrateDepInfo {
858 use cargo::core::dependency::DepKind;
859
860 let mut deps_by_name: BTreeMap<&str, Vec<&Dependency>> = BTreeMap::new();
861 for dep in manifest.dependencies() {
862 if include_dev_dependencies || dep.kind() != DepKind::Development {
864 let s = dep.name_in_toml().as_str();
865 deps_by_name.entry(s).or_default().push(dep);
866 }
867 }
868 let deps_by_name = deps_by_name;
869
870 let mut features_with_deps = BTreeMap::new();
871
872 let feature_map = manifest.summary().features();
874 for (feature, deps) in feature_map {
875 let mut feature_deps: Vec<&'static str> = vec![];
876 let mut other_deps: Vec<Dependency> = Vec::new();
877 for dep in deps {
878 use self::FeatureValue::{Dep, DepFeature, Feature};
879 match dep {
880 Feature(dep_feature) => {
882 feature_deps.push(InternedString::new(dep_feature).as_str());
883 }
884 Dep { dep_name } => {
886 for &dep in deps_by_name.get(dep_name.as_str()).unwrap() {
888 other_deps.push(dep.clone());
889 }
890 }
891 DepFeature {
893 dep_name,
894 dep_feature,
895 ..
896 } => {
897 if let Some(dd) = deps_by_name.get(dep_name.as_str()) {
898 for &dep in dd {
899 let mut dep = dep.clone();
900 let mut features: Vec<InternedString> =
901 vec![InternedString::new(dep_feature)];
902 features.extend(dep.features());
903 dep.set_features(features);
904 dep.set_default_features(false);
905 other_deps.push(dep);
906 }
907 } else {
908 let mut expected = false;
909 for dep in manifest.dependencies() {
910 if dep.kind() == DepKind::Development {
911 let s = dep.name_in_toml().as_str();
912 if s == dep_name.as_str() {
913 expected = true;
914 }
915 }
916 }
917 if expected {
918 debcargo_warn!(
919 "Ignoring \"{}\" feature \"{}\" as it depends on a \
920 dev-dependency \"{}\"",
921 manifest.package_id(),
922 feature,
923 dep_name
924 );
925 } else {
926 panic!(
927 "failed to account for dependency \"{}\" of \"{}\" feature \"{}\"",
928 dep_name,
929 manifest.package_id(),
930 feature
931 );
932 }
933 }
934 }
935 }
936 }
937
938 if feature_deps.is_empty()
939 || check_cycle_in_feature(feature_map, feature) && !feature_deps.contains(&"")
940 {
941 feature_deps.push("");
943 }
944
945 features_with_deps.insert(feature.as_str(), (feature_deps, other_deps));
946 }
947
948 let mut deps_required: Vec<Dependency> = Vec::new();
950 for deps in deps_by_name.values() {
951 for &dep in deps {
952 if !dep.is_optional() {
953 deps_required.push(dep.clone());
954 }
955 }
956 }
957
958 features_with_deps.insert("", (vec![], deps_required));
960
961 if !features_with_deps.contains_key("default") {
963 features_with_deps.insert("default", (vec![""], vec![]));
964 }
965
966 features_with_deps
967}
968
969fn add_unexpected_cfgs(doc: &mut toml_edit::DocumentMut, features: &[String]) {
975 use toml_edit::{Item, Table};
976
977 let cfg_value = |f: &str| format!("cfg(feature, values(\"{f}\"))");
978
979 let new_table = || {
980 let mut t = Table::new();
981 t.set_implicit(true);
982 Item::Table(t)
983 };
984 let unexpected = doc
985 .entry("lints")
986 .or_insert_with(new_table)
987 .as_table_mut()
988 .and_then(|t| t.entry("rust").or_insert_with(new_table).as_table_mut())
989 .map(|t| t.entry("unexpected_cfgs").or_insert_with(new_table));
990 let Some(unexpected) = unexpected.and_then(Item::as_table_mut) else {
993 debcargo_warn!("Cargo.toml: [lints.rust.unexpected_cfs] is not a TOML table, not injecting removed features!");
994 return;
995 };
996 unexpected.set_implicit(false);
998 unexpected
999 .entry("level")
1000 .or_insert_with(|| toml_edit::value("warn"));
1001 let Some(check_cfg) = unexpected
1002 .entry("check-cfg")
1003 .or_insert_with(|| toml_edit::value(toml_edit::Array::new()))
1004 .as_array_mut()
1005 else {
1006 return;
1007 };
1008
1009 let existing: HashSet<&str> = check_cfg
1010 .iter()
1011 .filter_map(toml_edit::Value::as_str)
1012 .collect();
1013 let additions: Vec<String> = features
1014 .iter()
1015 .map(|f| cfg_value(f))
1016 .filter(|v| !existing.contains(v.as_str()))
1017 .collect();
1018 for entry in additions {
1019 let formatted = toml_edit::Value::from(entry).decorated("\n ", "");
1021 check_cfg.push_formatted(formatted);
1022 }
1023}
1024
1025fn apply_remove_features(
1030 doc: toml_edit::DocumentMut,
1031 features: &[String],
1032) -> Result<Option<String>> {
1033 let mut manifest = cargo_toml_edit::Manifest::from(doc);
1034
1035 let mut filter = FeatureFilter::empty();
1036 filter.features = features.to_vec();
1037 filter.unsafe_mode = true;
1038 log::debug!("Feature filters: {filter:?}");
1039
1040 let filtered = manifest.filter_features(&filter)?;
1041 if filtered.removed_features.is_empty() {
1042 Ok(None)
1043 } else {
1044 debcargo_info!("Removed features:");
1045 for (feature, _item) in &filtered.removed_features {
1046 debcargo_info!(format!("- {feature}"));
1047 }
1048 debcargo_info!("Dependencies:");
1049 for (dependency, _opt) in &filtered.removed_dependencies {
1050 debcargo_info!(format!("- {dependency}"));
1051 }
1052
1053 let mut doc = toml_edit::DocumentMut::from_str(&manifest.to_string())?;
1054 add_unexpected_cfgs(&mut doc, features);
1055 Ok(Some(doc.to_string()))
1056 }
1057}
1058
1059pub fn remove_features_patch(
1069 path: &Path,
1070 features: &[String],
1071 auto_patches: &mut AutoPatches,
1072) -> Result<()> {
1073 let dir = path
1074 .parent()
1075 .ok_or_else(|| format_err!("{path:?} has no parent directory"))?;
1076
1077 apply_auto_patches(path, auto_patches)?;
1081 let base_content = fs::read_to_string(path)?;
1082
1083 let doc: toml_edit::DocumentMut = base_content.parse()?;
1086 let modified = apply_remove_features(doc, features)?;
1087
1088 let result = if let Some(modified) = modified {
1089 let base_path = dir.join("Cargo.toml.featured");
1090 let modified_path = dir.join("Cargo.toml.stripped");
1091 fs::write(&base_path, &base_content)?;
1092 fs::write(&modified_path, modified)?;
1093 let diff = diff_files("Cargo.toml", &base_path, &modified_path)?;
1094 fs::remove_file(&base_path)?;
1095 fs::remove_file(&modified_path)?;
1096 let mut patch =
1097 String::from("This patch was autogenerated using debcargo via `remove_features`. Do not edit manually!\n");
1098 patch.push_str(&diff);
1099 Some(patch.into_bytes())
1100 } else {
1101 None
1102 };
1103
1104 unapply_auto_patches(path, auto_patches)?;
1105
1106 if let Some(patch) = result {
1107 auto_patches.insert("auto-remove-features.patch".to_string(), patch);
1108 }
1109 Ok(())
1110}
1111
1112fn apply_remove_target_type(
1113 mut doc: toml_edit::DocumentMut,
1114 target_type: &TargetType,
1115) -> Result<Option<String>> {
1116 log::debug!("Removing target type {target_type}");
1117 let mut modified = false;
1118 let Some(package_item) = doc.get_mut("package") else {
1119 debcargo_bail!("Cargo.toml must contain 'package' section");
1120 };
1121 let Some(package_table) = package_item.as_table_mut() else {
1122 debcargo_bail!("Cargo.toml must contain 'package' table");
1123 };
1124
1125 let autokey = match target_type {
1126 TargetType::Bench => "autobenches",
1127 TargetType::Example => "autoexamples",
1128 TargetType::Test => "autotests",
1129 };
1130
1131 if let Some(value) = package_table.get_mut(autokey) {
1132 let Some(enabled) = value.as_bool() else {
1133 debcargo_bail!("Malformed manifest: package.{autokey} is not a boolean value: {item}");
1134 };
1135 if enabled {
1136 modified = true;
1137 *value = false.into();
1138 }
1139 } else {
1140 modified = true;
1141 package_table.insert(autokey, false.into());
1142 }
1143
1144 let section_key = match target_type {
1145 TargetType::Bench => "bench",
1146 TargetType::Example => "example",
1147 TargetType::Test => "test",
1148 };
1149 let removed = doc.remove(section_key);
1150 if removed.is_some() {
1151 modified = true;
1152 }
1153
1154 if modified {
1155 return Ok(Some(doc.to_string()));
1156 }
1157
1158 Ok(None)
1159}
1160
1161pub fn remove_target_types_patch(
1162 path: &Path,
1163 target_types: &[TargetType],
1164 auto_patches: &mut AutoPatches,
1165) -> Result<()> {
1166 let dir = path
1167 .parent()
1168 .ok_or_else(|| format_err!("{path:?} has no parent directory"))?;
1169
1170 apply_auto_patches(path, auto_patches)?;
1174 for target_type in target_types {
1175 let base_content = fs::read_to_string(path)?;
1176
1177 let doc: toml_edit::DocumentMut = base_content.parse()?;
1180 let modified = apply_remove_target_type(doc, target_type)?;
1181
1182 let result = if let Some(modified) = modified {
1183 let base_path = dir.join("Cargo.toml.base");
1184 let modified_path = dir.join(format!("Cargo.toml.stripped_{target_type}"));
1185 fs::write(&base_path, &base_content)?;
1186 fs::write(&modified_path, &modified)?;
1187 let diff = diff_files("Cargo.toml", &base_path, &modified_path)?;
1188 fs::remove_file(&base_path)?;
1189 fs::remove_file(&modified_path)?;
1190 let mut patch = String::from(
1191 "This patch was autogenerated using debcargo via `remove_target_types`. Do not edit manually!\n",
1192 );
1193 patch.push_str(&diff);
1194 Some(patch.into_bytes())
1195 } else {
1196 None
1197 };
1198 if let Some(patch) = result {
1199 run_patch(path, &patch, false)?;
1200 auto_patches.insert(format!("auto-remove-{target_type}.patch"), patch);
1201 }
1202 }
1203
1204 unapply_auto_patches(path, auto_patches)?;
1205
1206 Ok(())
1207}
1208
1209fn apply_remove_targets(
1210 doc: toml_edit::DocumentMut,
1211 filter_targets: &[FilterTarget],
1212) -> Result<Option<String>> {
1213 let mut manifest = cargo_toml_edit::Manifest::from(doc);
1214
1215 let targets: Result<Vec<_>> = filter_targets
1216 .iter()
1217 .flat_map(FilterTarget::target_list)
1218 .map(|t| {
1219 get_builtin_target_by_triple(t)
1220 .ok_or_else(|| format_err!("Triple '{t}' is not known to cfg-expr"))
1221 })
1222 .collect();
1223 let targets = targets?;
1224 let mut filter = DependencyFilter::empty();
1225 filter.targets = targets;
1226 log::debug!("Target filters: {filter:?}");
1227
1228 let filtered = manifest.filter_dependencies(&filter)?;
1229 if filtered.removed_targets.is_empty() {
1230 Ok(None)
1231 } else {
1232 debcargo_info!("Removed dependencies of following targets:");
1233 for (target, _item) in &filtered.removed_targets {
1234 debcargo_info!(format!("- {target}"));
1235 }
1236 debcargo_info!("Dependencies:");
1237 for (dependency, _opt) in &filtered.removed_dependencies {
1238 debcargo_info!(format!("- {dependency}"));
1239 }
1240 Ok(Some(manifest.to_string()))
1241 }
1242}
1243
1244pub fn remove_targets_patch(
1253 path: &Path,
1254 filter_targets: &[FilterTarget],
1255 auto_patches: &mut AutoPatches,
1256) -> Result<()> {
1257 let dir = path
1258 .parent()
1259 .ok_or_else(|| format_err!("{path:?} has no parent directory"))?;
1260
1261 apply_auto_patches(path, auto_patches)?;
1265 let base_content = fs::read_to_string(path)?;
1266
1267 let doc: toml_edit::DocumentMut = base_content.parse()?;
1270 let modified = apply_remove_targets(doc, filter_targets)?;
1271
1272 let result = if let Some(modified) = modified {
1273 let base_path = dir.join("Cargo.toml.all_targets");
1274 let modified_path = dir.join("Cargo.toml.stripped_targets");
1275 fs::write(&base_path, &base_content)?;
1276 fs::write(&modified_path, &modified)?;
1277 let diff = diff_files("Cargo.toml", &base_path, &modified_path)?;
1278 fs::remove_file(&base_path)?;
1279 fs::remove_file(&modified_path)?;
1280 let mut patch =
1281 String::from("This patch was autogenerated using debcargo via `remove_targets`. Do not edit manually!\n");
1282 patch.push_str(&diff);
1283 Some(patch.into_bytes())
1284 } else {
1285 None
1286 };
1287
1288 unapply_auto_patches(path, auto_patches)?;
1289
1290 if let Some(patch) = result {
1291 auto_patches.insert("auto-remove-targets.patch".to_string(), patch);
1292 }
1293 Ok(())
1294}
1295
1296fn apply_auto_patches(path: &Path, auto_patches: &AutoPatches) -> Result<()> {
1298 for patch in auto_patches.values() {
1299 run_patch(path, patch, false)?;
1300 }
1301 Ok(())
1302}
1303
1304fn unapply_auto_patches(path: &Path, auto_patches: &AutoPatches) -> Result<()> {
1307 for patch in auto_patches.values().rev() {
1308 run_patch(path, patch, true)?;
1309 }
1310 Ok(())
1311}
1312
1313fn run_patch(path: &Path, patch: &[u8], reverse: bool) -> Result<()> {
1316 use std::io::Write;
1317 use std::process::Stdio;
1318
1319 let mut cmd = std::process::Command::new("patch");
1320 cmd.arg("--quiet").arg(path);
1321 if reverse {
1322 cmd.arg("--reverse");
1323 }
1324 let mut child = cmd.stdin(Stdio::piped()).spawn()?;
1325 child
1326 .stdin
1327 .take()
1328 .ok_or_else(|| format_err!("failed to open stdin for patch"))?
1329 .write_all(patch)?;
1330 let status = child.wait()?;
1331 if !status.success() {
1332 debcargo_bail!("failed to apply auto-patch to {:?}", path);
1333 }
1334 Ok(())
1335}
1336
1337pub fn transitive_deps<'a>(
1341 features_with_deps: &'a CrateDepInfo,
1342 feature: &'a str,
1343) -> Result<(Vec<&'a str>, Vec<Dependency>)> {
1344 fn transitive_deps_impl<'a>(
1345 features_with_deps: &'a CrateDepInfo,
1346 feature: &'a str,
1347 cache: &mut HashMap<&'a str, (IndexSet<&'a str>, IndexSet<Dependency>)>,
1348 processing: &mut Vec<&'a str>,
1349 ) -> Result<(IndexSet<&'a str>, IndexSet<Dependency>)> {
1350 let mut all_features = IndexSet::new();
1351 let mut all_deps = IndexSet::new();
1352 processing.push(feature);
1353 if let Some((ff, dd)) = features_with_deps.get(feature) {
1354 all_features.extend(ff.clone());
1355 all_deps.extend(dd.clone());
1356
1357 for f in ff {
1358 if processing.contains(f) {
1359 continue;
1360 }
1361
1362 let (ff1, dd1) = if let Some(cached) = cache.get(f) {
1363 cached.clone()
1364 } else {
1365 transitive_deps_impl(features_with_deps, f, cache, processing)?
1366 };
1367
1368 let ff1: Vec<_> = ff1
1369 .iter()
1370 .filter(|f| **f != feature && !processing.contains(f))
1371 .collect();
1372
1373 all_features.extend(ff1);
1374 all_deps.extend(dd1);
1375 }
1376 } else {
1377 log::trace!("{features_with_deps:?}");
1378 debcargo_bail!(
1379 "requested feature '{}' not found in resolved map of features+dependencies",
1380 feature
1381 );
1382 }
1383 let result = (all_features, all_deps);
1384 cache.insert(feature, result.clone());
1385 processing.pop();
1386 Ok(result)
1387 }
1388
1389 let mut cache = HashMap::new();
1390 let mut processing = Vec::new();
1391 let (all_features, all_deps) =
1392 transitive_deps_impl(features_with_deps, feature, &mut cache, &mut processing)?;
1393
1394 Ok((
1395 all_features.into_iter().collect_vec(),
1396 all_deps.into_iter().collect_vec(),
1397 ))
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402 use super::*;
1403
1404 fn doc_from(s: &str) -> toml_edit::DocumentMut {
1405 s.parse().unwrap()
1406 }
1407
1408 #[test]
1409 fn apply_remove_features_keeps_dep_referenced_only_via_slash_syntax() {
1410 let doc = doc_from(
1411 r#"
1412[package]
1413name = "test-crate"
1414version = "0.1.0"
1415edition = "2021"
1416
1417[features]
1418default = []
1419unstable = ["bar/some-feature"]
1420stable = ["bar?/other-feature"]
1421
1422[dependencies]
1423bar = { version = "1", optional = true }
1424"#,
1425 );
1426 let modified =
1427 apply_remove_features(doc.clone(), &["unstable".to_string(), "stable".to_string()])
1428 .expect("Removing features should not return error")
1429 .expect("Removing features should return modified Cargo.toml");
1430 let doc = doc_from(&modified);
1431 let deps = doc["dependencies"].as_table().unwrap();
1434 assert!(deps.contains_key("bar"));
1435 }
1436
1437 #[test]
1438 fn apply_remove_features_noop_when_feature_absent() {
1439 let doc = doc_from(
1440 r#"
1441[package]
1442name = "test-crate"
1443version = "0.1.0"
1444edition = "2021"
1445
1446[features]
1447stable = []
1448"#,
1449 );
1450 assert!(
1451 apply_remove_features(doc.clone(), &["nonexistent".to_string()])
1452 .expect("Removing features should not return error")
1453 .is_none()
1454 );
1455 }
1456
1457 #[test]
1458 fn apply_remove_features_removes_named_feature() {
1459 let doc = doc_from(
1460 r#"
1461[features]
1462default = ["stable"]
1463stable = []
1464unstable = []
1465"#,
1466 );
1467 let modified = apply_remove_features(doc.clone(), &["unstable".to_string()])
1468 .expect("Removing features should not return error")
1469 .expect("Removing features should return modified Cargo.toml");
1470 let doc = doc_from(&modified);
1471 let feats = doc["features"].as_table().unwrap();
1472 assert!(!feats.contains_key("unstable"));
1473 assert!(feats.contains_key("stable"));
1474 }
1475
1476 #[test]
1477 fn apply_remove_features_minimal_diff_preserves_formatting() {
1478 let doc = doc_from(
1480 r#"[features]
1481default = []
1482keep = [
1483 "a",
1484 "b",
1485]
1486drop = []
1487"#,
1488 );
1489 let out = apply_remove_features(doc.clone(), &["drop".to_string()])
1490 .expect("Removing features should not return error")
1491 .expect("Removing features should return modified Cargo.toml");
1492 assert!(out.contains("keep = [\n \"a\",\n \"b\",\n]"));
1493 assert!(!out.contains("drop = []"));
1494 }
1495}