1use crate::{
4 CcActionContext, CcActionInput, CcBypassReason, CcCompilerFamily, MAX_INPUT_BYTES,
5 MAX_MANIFEST_ENTRIES, MAX_PREDICTED_INPUTS, normalize_components,
6};
7use mbx_cache_core::{
8 CacheDigest, FileDigestCache, FileDigestResolution, FileDigestScope, FileIdentity,
9 FileSnapshot, RecordedFileDigest, digest_file, digest_file_validated,
10};
11use std::collections::{BTreeMap, BTreeSet};
12use std::io::Read;
13use std::path::{Path, PathBuf};
14use std::time::SystemTime;
15
16pub const INCLUDE_MANIFEST_PREFIX: &str = "@include-manifest:";
18
19const ASSEMBLER_INPUT_DIRECTIVES: &[&[u8]] = &[b".include", b".incbin", b".sinclude"];
22
23const SCAN_CHUNK_BYTES: usize = 64 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Eq, Default)]
27pub struct CcDepfile {
28 pub files: Vec<PathBuf>,
30}
31
32impl CcDepfile {
33 pub fn read(path: &Path) -> Result<Self, CcBypassReason> {
35 let contents =
36 std::fs::read_to_string(path).map_err(|error| CcBypassReason::DepfileRead {
37 path: path.to_path_buf(),
38 message: error.to_string(),
39 })?;
40 Self::parse(&contents)
41 }
42
43 pub fn read_for(path: &Path, family: CcCompilerFamily) -> Result<Self, CcBypassReason> {
45 if family.is_msvc() {
46 Self::read_msvc(path)
47 } else {
48 Self::read(path)
49 }
50 }
51
52 pub fn read_msvc(path: &Path) -> Result<Self, CcBypassReason> {
54 let contents = std::fs::read(path).map_err(|error| CcBypassReason::DepfileRead {
55 path: path.to_path_buf(),
56 message: error.to_string(),
57 })?;
58 let value: serde_json::Value = serde_json::from_slice(&contents)
59 .map_err(|error| CcBypassReason::MalformedDepfile(error.to_string()))?;
60 let data = value
61 .get("Data")
62 .and_then(serde_json::Value::as_object)
63 .ok_or_else(|| CcBypassReason::MalformedDepfile("missing Data object".into()))?;
64 if data
65 .get("ImportedModules")
66 .and_then(serde_json::Value::as_array)
67 .is_some_and(|modules| !modules.is_empty())
68 || data.get("ProvidedModule").is_some_and(|module| {
69 !module.is_null() && module.as_str().is_none_or(|s| !s.is_empty())
70 })
71 {
72 return Err(CcBypassReason::MalformedDepfile(
73 "C++ module dependencies are not modeled".into(),
74 ));
75 }
76 let includes = data
77 .get("Includes")
78 .and_then(serde_json::Value::as_array)
79 .ok_or_else(|| CcBypassReason::MalformedDepfile("missing Includes array".into()))?;
80 let files = includes
81 .iter()
82 .map(|entry| {
83 entry.as_str().map(PathBuf::from).ok_or_else(|| {
84 CcBypassReason::MalformedDepfile("non-string include path".into())
85 })
86 })
87 .collect::<Result<Vec<_>, _>>()?;
88 Ok(Self { files })
89 }
90
91 pub fn parse(contents: &str) -> Result<Self, CcBypassReason> {
97 let joined = join_continuations(contents)?;
98 let (_, prerequisites) = joined
99 .lines()
100 .find_map(|line| line.split_once(RULE_SEPARATOR))
101 .ok_or_else(|| CcBypassReason::MalformedDepfile("no dependency rule".into()))?;
102 let files = split_prerequisites(prerequisites)?;
103 Ok(Self { files })
104 }
105}
106
107const RULE_SEPARATOR: &str = ": ";
108
109impl CcDepfile {
110 pub fn render(
120 targets: &[crate::DepfileTarget],
121 files: &[PathBuf],
122 source: &Path,
123 phony_targets: bool,
124 ) -> String {
125 let mut rendered = String::new();
126 for (index, target) in targets.iter().enumerate() {
127 if index > 0 {
128 rendered.push(' ');
129 }
130 if target.quoted {
131 rendered.push_str(&escape_make_word(&target.name));
132 } else {
133 rendered.push_str(&target.name);
134 }
135 }
136 rendered.push(':');
137 for file in files {
138 rendered.push_str(" \\\n ");
139 rendered.push_str(&escape_make_word(&file.to_string_lossy()));
140 }
141 rendered.push('\n');
142 if phony_targets {
143 for file in files.iter().filter(|file| file.as_path() != source) {
144 rendered.push_str(&escape_make_word(&file.to_string_lossy()));
145 rendered.push_str(":\n");
146 }
147 }
148 rendered
149 }
150}
151
152fn escape_make_word(word: &str) -> String {
154 let mut escaped = String::with_capacity(word.len());
155 for character in word.chars() {
156 match character {
157 ' ' => escaped.push_str("\\ "),
158 '#' => escaped.push_str("\\#"),
159 '$' => escaped.push_str("$$"),
160 other => escaped.push(other),
161 }
162 }
163 escaped
164}
165
166fn join_continuations(contents: &str) -> Result<String, CcBypassReason> {
168 let mut joined = String::with_capacity(contents.len());
169 let mut continued = false;
170 for line in contents.lines() {
171 let trimmed = line.strip_suffix('\r').unwrap_or(line);
172 let (text, continues) = match trimmed.strip_suffix('\\') {
173 Some(text) => (text, true),
174 None => (trimmed, false),
175 };
176 if continued {
177 joined.push(' ');
178 }
179 joined.push_str(text.trim_end_matches(['\t']));
180 if !continues {
181 joined.push('\n');
182 }
183 continued = continues;
184 }
185 if continued {
186 return Err(CcBypassReason::MalformedDepfile(
187 "unterminated line continuation".into(),
188 ));
189 }
190 Ok(joined)
191}
192
193fn split_prerequisites(value: &str) -> Result<Vec<PathBuf>, CcBypassReason> {
198 if memchr::memchr2(b'\\', b'$', value.as_bytes()).is_none() {
201 return Ok(value
202 .split([' ', '\t'])
203 .filter(|word| !word.is_empty())
204 .map(PathBuf::from)
205 .collect());
206 }
207 split_escaped_prerequisites(value)
208}
209
210fn split_escaped_prerequisites(value: &str) -> Result<Vec<PathBuf>, CcBypassReason> {
211 let mut files = Vec::new();
212 let mut current = String::new();
213 let mut characters = value.chars().peekable();
214 while let Some(character) = characters.next() {
215 match character {
216 ' ' | '\t' => {
217 if !current.is_empty() {
218 files.push(PathBuf::from(std::mem::take(&mut current)));
219 }
220 }
221 '\\' => match characters.next() {
222 Some(' ') => current.push(' '),
223 Some('#') => current.push('#'),
224 Some(other) => {
225 return Err(CcBypassReason::MalformedDepfile(format!(
226 "unmodeled escape \\{other}"
227 )));
228 }
229 None => {
230 return Err(CcBypassReason::MalformedDepfile(
231 "trailing escape character".into(),
232 ));
233 }
234 },
235 '$' => match characters.next() {
236 Some('$') => current.push('$'),
237 Some(other) => {
238 return Err(CcBypassReason::MalformedDepfile(format!(
239 "unmodeled variable reference ${other}"
240 )));
241 }
242 None => {
243 return Err(CcBypassReason::MalformedDepfile(
244 "trailing variable reference".into(),
245 ));
246 }
247 },
248 other => current.push(other),
249 }
250 }
251 if !current.is_empty() {
252 files.push(PathBuf::from(current));
253 }
254 Ok(files)
255}
256
257#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct CcDiscoveredInputs {
260 working_dir: PathBuf,
261 pub inputs: Vec<CcActionInput>,
263 identities: Vec<Option<FileIdentity>>,
267}
268
269impl CcDiscoveredInputs {
270 pub fn collect(
277 working_dir: &Path,
278 files: BTreeSet<PathBuf>,
279 directories: BTreeSet<PathBuf>,
280 digests: &dyn FileDigestCache,
281 ) -> Result<Self, CcBypassReason> {
282 if !working_dir.is_absolute() {
283 return Err(CcBypassReason::RelativeWorkingDirectory(
284 working_dir.to_path_buf(),
285 ));
286 }
287 let directories = minimal_manifest_directories(directories);
288 if files.len() + directories.len() > MAX_PREDICTED_INPUTS {
289 return Err(CcBypassReason::TooManyInputs);
290 }
291 let working_dir = normalize_components(working_dir);
292 let mut inputs = Vec::with_capacity(files.len() + directories.len());
293 let mut total_bytes = 0_u64;
294 let mut identified = Vec::with_capacity(files.len());
301 for path in files {
302 let metadata = std::fs::metadata(&path).map_err(|error| CcBypassReason::InputRead {
303 path: path.clone(),
304 message: error.to_string(),
305 })?;
306 if !metadata.is_file() {
307 return Err(CcBypassReason::InputRead {
308 path,
309 message: "input is not a regular file".into(),
310 });
311 }
312 total_bytes = total_bytes.saturating_add(metadata.len());
313 if total_bytes > MAX_INPUT_BYTES {
314 return Err(CcBypassReason::TooManyInputs);
315 }
316 let identity = FileIdentity::for_digest_cache(&path, &metadata).map_err(|error| {
317 CcBypassReason::InputRead {
318 path: path.clone(),
319 message: error.to_string(),
320 }
321 })?;
322 identified.push((path, identity));
323 }
324 let queries = identified
325 .iter()
326 .filter_map(|(_, identity)| identity.clone())
327 .collect::<Vec<_>>();
328 let mut recorded = digests
329 .resolve(FileDigestScope::CcInput, &queries)
330 .into_iter();
331 let mut identities = Vec::with_capacity(inputs.capacity());
332 let mut fresh = Vec::new();
333 for (path, identity) in identified {
334 let resolution = identity
335 .as_ref()
336 .and_then(|_| recorded.next())
337 .unwrap_or(FileDigestResolution::Unresolved);
338 let cached = match resolution {
339 FileDigestResolution::Digest(digest)
340 if identity.as_ref().is_some_and(|identity| {
341 identity.len == digest.size && identity.still_describes().unwrap_or(false)
342 }) =>
343 {
344 Some(Ok(digest))
345 }
346 FileDigestResolution::EmbeddedTimestampMacro
347 if identity
348 .as_ref()
349 .is_some_and(|identity| identity.still_describes().unwrap_or(false)) =>
350 {
351 Some(Err(CcBypassReason::EmbeddedTimestampMacro(path.clone())))
352 }
353 FileDigestResolution::Digest(_)
354 | FileDigestResolution::EmbeddedTimestampMacro
355 | FileDigestResolution::Unresolved => None,
356 };
357 let digest = if let Some(cached) = cached {
358 identities.push(identity);
359 cached?
360 } else {
361 let observed =
362 digest_file_validated(FileDigestScope::CcInput, &path).map_err(|error| {
363 CcBypassReason::InputRead {
364 path: path.clone(),
365 message: error.to_string(),
366 }
367 })?;
368 let (resolution, observed_identity) = if let Some(observed) = observed {
369 let identity = observed.cache_identity.clone();
370 if let FileDigestResolution::Digest(digest) = &observed.resolution
371 && let Some(file) = observed.cache_identity
372 && file.len == digest.size
373 {
374 fresh.push(RecordedFileDigest {
375 file,
376 digest: digest.clone(),
377 });
378 }
379 (observed.resolution, identity)
380 } else {
381 let resolution =
382 digest_file(FileDigestScope::CcInput, &path).map_err(|error| {
383 CcBypassReason::InputRead {
384 path: path.clone(),
385 message: error.to_string(),
386 }
387 })?;
388 (resolution, None)
389 };
390 let FileDigestResolution::Digest(digest) = resolution else {
391 return Err(CcBypassReason::EmbeddedTimestampMacro(path));
392 };
393 identities.push(observed_identity);
394 digest
395 };
396 inputs.push(CcActionInput { path, digest });
397 }
398 if !fresh.is_empty() {
399 digests.record(FileDigestScope::CcInput, fresh);
400 }
401 let mut manifest_entries = 0_usize;
402 for directory in directories {
403 let digest = include_manifest(&directory, &mut manifest_entries)?;
404 inputs.push(CcActionInput {
405 path: PathBuf::from(format!("{INCLUDE_MANIFEST_PREFIX}{}", directory.display())),
406 digest,
407 });
408 identities.push(None);
409 }
410 Ok(Self {
411 working_dir,
412 inputs,
413 identities,
414 })
415 }
416
417 pub fn files(&self) -> impl Iterator<Item = &CcActionInput> {
419 self.inputs
420 .iter()
421 .filter(|input| !is_manifest_input(&input.path))
422 }
423
424 pub fn verify_not_modified_since(&self, started_at: SystemTime) -> Result<(), CcBypassReason> {
431 self.verify_not_modified_since_with_snapshots(started_at, &BTreeMap::new())
432 }
433
434 pub fn verify_not_modified_since_with_snapshots(
437 &self,
438 started_at: SystemTime,
439 before: &BTreeMap<PathBuf, FileSnapshot>,
440 ) -> Result<(), CcBypassReason> {
441 for input in self.files() {
442 if let Some(previous) = before.get(&input.path)
443 && previous.proves_content_change()
444 {
445 let metadata =
446 std::fs::metadata(&input.path).map_err(|error| CcBypassReason::InputRead {
447 path: input.path.clone(),
448 message: error.to_string(),
449 })?;
450 let identity = FileIdentity::for_digest_cache(&input.path, &metadata)
451 .map_err(|error| CcBypassReason::InputRead {
452 path: input.path.clone(),
453 message: error.to_string(),
454 })?
455 .or_else(|| FileIdentity::describe(&input.path, &metadata));
456 if previous.matches(identity.as_ref(), &input.digest) {
457 continue;
458 }
459 return Err(CcBypassReason::InputModifiedDuringCompilation(
460 input.path.clone(),
461 ));
462 }
463 let metadata =
464 std::fs::metadata(&input.path).map_err(|error| CcBypassReason::InputRead {
465 path: input.path.clone(),
466 message: error.to_string(),
467 })?;
468 let modified = metadata
469 .modified()
470 .map_err(|error| CcBypassReason::InputRead {
471 path: input.path.clone(),
472 message: error.to_string(),
473 })?;
474 if modified >= started_at {
475 return Err(CcBypassReason::InputModifiedDuringCompilation(
476 input.path.clone(),
477 ));
478 }
479 }
480 Ok(())
481 }
482
483 pub fn verify_not_modified_since_with_identities(
485 &self,
486 started_at: SystemTime,
487 before: &BTreeMap<PathBuf, FileIdentity>,
488 ) -> Result<(), CcBypassReason> {
489 let snapshots = before
490 .iter()
491 .map(|(path, identity)| (path.clone(), identity.clone().into()))
492 .collect();
493 self.verify_not_modified_since_with_snapshots(started_at, &snapshots)
494 }
495
496 pub fn verify(&self) -> Result<(), CcBypassReason> {
505 for (index, input) in self.inputs.iter().enumerate() {
506 if is_manifest_input(&input.path) {
507 continue;
508 }
509 let read_error = |error: std::io::Error| CcBypassReason::InputRead {
510 path: input.path.clone(),
511 message: error.to_string(),
512 };
513 if let Some(Some(identity)) = self.identities.get(index)
514 && identity.can_skip_content_verification()
515 && identity.still_describes().map_err(read_error)?
516 {
517 continue;
518 }
519 let matches = input.digest.matches_file(&input.path).map_err(|error| {
520 CcBypassReason::InputRead {
521 path: input.path.clone(),
522 message: error.to_string(),
523 }
524 })?;
525 if !matches {
526 return Err(CcBypassReason::InputChanged(input.path.clone()));
527 }
528 }
529 Ok(())
530 }
531
532 pub fn apply_to(self, context: &mut CcActionContext) -> Result<(), CcBypassReason> {
535 if normalize_components(&context.working_dir) != self.working_dir {
536 return Err(CcBypassReason::DiscoveryWorkingDirectory);
537 }
538 context.inputs.extend(self.inputs);
539 Ok(())
540 }
541}
542
543fn minimal_manifest_directories(directories: BTreeSet<PathBuf>) -> Vec<PathBuf> {
551 let mut directories = directories
552 .into_iter()
553 .map(|directory| {
554 let normalized = manifest_directory_identity(&directory);
555 (directory, normalized)
556 })
557 .collect::<Vec<_>>();
558 directories.sort_by(|(left, left_normalized), (right, right_normalized)| {
559 left_normalized
560 .as_deref()
561 .unwrap_or(left)
562 .components()
563 .count()
564 .cmp(
565 &right_normalized
566 .as_deref()
567 .unwrap_or(right)
568 .components()
569 .count(),
570 )
571 .then_with(|| left_normalized.cmp(right_normalized))
572 .then_with(|| left.cmp(right))
573 });
574
575 let mut minimal = Vec::<(PathBuf, Option<PathBuf>)>::new();
576 for (directory, normalized) in directories {
577 if !minimal
578 .iter()
579 .any(|(_, ancestor)| match (ancestor, &normalized) {
580 (Some(ancestor), Some(normalized)) => manifest_covers(ancestor, normalized),
581 _ => false,
582 })
583 {
584 minimal.push((directory, normalized));
585 }
586 }
587 minimal
588 .into_iter()
589 .map(|(directory, _)| directory)
590 .collect()
591}
592
593fn manifest_directory_identity(directory: &Path) -> Option<PathBuf> {
597 let mut normalized = PathBuf::new();
598 for component in directory.components() {
599 match component {
600 std::path::Component::CurDir => {}
601 std::path::Component::ParentDir => {
602 let metadata = std::fs::symlink_metadata(&normalized).ok()?;
603 if !metadata.is_dir() || metadata.file_type().is_symlink() {
604 return None;
605 }
606 normalized.pop();
607 }
608 component => normalized.push(component.as_os_str()),
609 }
610 }
611 Some(normalized)
612}
613
614fn manifest_covers(ancestor: &Path, descendant: &Path) -> bool {
621 let Ok(relative) = descendant.strip_prefix(ancestor) else {
622 return false;
623 };
624 if relative.as_os_str().is_empty() {
625 return true;
626 }
627 let mut current = ancestor.to_path_buf();
628 for component in relative.components() {
629 current.push(component);
630 let Ok(metadata) = std::fs::symlink_metadata(¤t) else {
631 return false;
632 };
633 if !metadata.is_dir() || metadata.file_type().is_symlink() {
634 return false;
635 }
636 }
637 true
638}
639
640fn is_manifest_input(path: &Path) -> bool {
641 path.to_str()
642 .is_some_and(|path| path.starts_with(INCLUDE_MANIFEST_PREFIX))
643}
644
645pub fn manifest_snapshot(
653 directories: &BTreeSet<PathBuf>,
654) -> Result<BTreeMap<PathBuf, CacheDigest>, CcBypassReason> {
655 let mut budget = 0_usize;
656 minimal_manifest_directories(directories.iter().cloned().collect())
657 .into_iter()
658 .map(|directory| {
659 include_manifest(&directory, &mut budget).map(|digest| (directory, digest))
660 })
661 .collect()
662}
663
664const INCLUDABLE_EXTENSIONS: &[&str] = &[
675 "c", "c++", "cc", "cpp", "cxx", "def", "gch", "h", "h++", "hh", "hpp", "hxx", "inc", "inl",
676 "ipp", "pch", "s", "tcc",
677];
678
679fn is_includable(name: &str) -> bool {
688 match name.rsplit_once('.') {
689 Some((stem, extension)) if !stem.is_empty() => INCLUDABLE_EXTENSIONS
690 .binary_search(&extension.to_ascii_lowercase().as_str())
691 .is_ok(),
692 _ => !name.starts_with('.'),
694 }
695}
696
697fn include_manifest(directory: &Path, budget: &mut usize) -> Result<CacheDigest, CcBypassReason> {
704 include_manifest_with(directory, budget, &|path| std::fs::read_dir(path))
705}
706
707fn include_manifest_with(
708 directory: &Path,
709 budget: &mut usize,
710 read_dir: &impl Fn(&Path) -> std::io::Result<std::fs::ReadDir>,
711) -> Result<CacheDigest, CcBypassReason> {
712 #[cfg(unix)]
713 if let Some(memo) = manifest_memo::find(directory) {
714 *budget = budget.saturating_add(memo.entries);
715 if *budget > MAX_MANIFEST_ENTRIES {
716 return Err(CcBypassReason::TooManyInputs);
717 }
718 return Ok(memo.digest);
719 }
720 #[cfg(unix)]
721 let mut directories = Vec::new();
722 #[cfg(unix)]
723 let initial_budget = *budget;
724 let mut names = Vec::new();
725 let mut pending = vec![(directory.to_path_buf(), String::new())];
726 while let Some((current, prefix)) = pending.pop() {
727 #[cfg(unix)]
730 directories.push((current.clone(), manifest_memo::Identity::read(¤t)));
731 let entries = match read_dir(¤t) {
732 Ok(entries) => entries,
733 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
734 Err(error) => {
735 return Err(CcBypassReason::InputRead {
736 path: current,
737 message: error.to_string(),
738 });
739 }
740 };
741 for entry in entries {
742 let entry = entry.map_err(|error| CcBypassReason::InputRead {
743 path: current.clone(),
744 message: error.to_string(),
745 })?;
746 let name = entry.file_name();
747 let Some(name) = name.to_str() else {
748 return Err(CcBypassReason::NonUtf8Path(entry.path()));
749 };
750 let relative = if prefix.is_empty() {
751 name.to_string()
752 } else {
753 format!("{prefix}/{name}")
754 };
755 let file_type = entry
756 .file_type()
757 .map_err(|error| CcBypassReason::InputRead {
758 path: entry.path(),
759 message: error.to_string(),
760 })?;
761 if file_type.is_dir() {
762 pending.push((entry.path(), relative));
763 continue;
764 }
765 if !is_includable(name) {
766 continue;
767 }
768 *budget += 1;
769 if *budget > MAX_MANIFEST_ENTRIES {
770 return Err(CcBypassReason::TooManyInputs);
771 }
772 names.push(relative);
773 }
774 }
775 names.sort();
776 let digest = CacheDigest::blake3(names.join("\n").as_bytes());
777 #[cfg(unix)]
778 manifest_memo::record(directory, &digest, *budget - initial_budget, directories);
779 Ok(digest)
780}
781
782#[cfg(unix)]
786mod manifest_memo {
787 use super::*;
788 use std::os::unix::fs::MetadataExt;
789 use std::sync::{Mutex, OnceLock};
790
791 const MAX_ROOTS: usize = 32;
792 const MAX_DIRECTORIES: usize = 4096;
793 static MEMOS: OnceLock<Mutex<BTreeMap<PathBuf, Memo>>> = OnceLock::new();
794
795 #[derive(Clone, PartialEq, Eq)]
796 pub(super) struct Identity {
797 device: u64,
798 inode: u64,
799 modified: (i64, i64),
800 changed: (i64, i64),
801 mode: u32,
802 }
803 impl Identity {
804 pub(super) fn read(path: &Path) -> Option<Self> {
805 let metadata = std::fs::metadata(path).ok()?;
806 if !metadata.is_dir() || metadata.mtime_nsec() == 0 || metadata.ctime_nsec() == 0 {
809 return None;
810 }
811 let changed = FileIdentity::for_digest_cache(path, &metadata)
814 .ok()??
815 .changed?;
816 Some(Self {
817 device: metadata.dev(),
818 inode: metadata.ino(),
819 modified: (metadata.mtime(), metadata.mtime_nsec()),
820 changed,
821 mode: metadata.mode(),
822 })
823 }
824 }
825 #[derive(Clone)]
826 pub(super) struct Memo {
827 pub digest: CacheDigest,
828 pub entries: usize,
829 directories: Vec<(PathBuf, Identity)>,
830 }
831 impl Memo {
832 fn valid(&self) -> bool {
833 self.directories
834 .iter()
835 .all(|(path, identity)| Identity::read(path).as_ref() == Some(identity))
836 }
837 }
838 pub(super) fn find(directory: &Path) -> Option<Memo> {
839 let memo = MEMOS.get()?.lock().ok()?.get(directory)?.clone();
840 memo.valid().then_some(memo)
841 }
842 pub(super) fn record(
843 directory: &Path,
844 digest: &CacheDigest,
845 entries: usize,
846 directories: Vec<(PathBuf, Option<Identity>)>,
847 ) {
848 if directories.is_empty() || directories.len() > MAX_DIRECTORIES {
849 return;
850 }
851 let Some(directories) = directories
852 .into_iter()
853 .map(|(path, identity)| Some((path, identity?)))
854 .collect::<Option<Vec<_>>>()
855 else {
856 return;
857 };
858 let memo = Memo {
859 digest: digest.clone(),
860 entries,
861 directories,
862 };
863 if !memo.valid() {
864 return;
865 }
866 let Ok(mut memos) = MEMOS.get_or_init(Default::default).lock() else {
867 return;
868 };
869 if memos.len() >= MAX_ROOTS {
870 memos.clear();
871 }
872 memos.insert(directory.to_path_buf(), memo);
873 }
874}
875
876pub(crate) fn contains_assembler_input_directive(path: &Path) -> Result<bool, CcBypassReason> {
883 let needles = ASSEMBLER_INPUT_DIRECTIVES;
884 let file = std::fs::File::open(path).map_err(|error| CcBypassReason::InputRead {
885 path: path.to_path_buf(),
886 message: error.to_string(),
887 })?;
888 let longest = needles
889 .iter()
890 .map(|needle| needle.len())
891 .max()
892 .unwrap_or_default();
893 let mut reader = std::io::BufReader::new(file);
894 let mut window = Vec::with_capacity(SCAN_CHUNK_BYTES + longest);
895 let mut chunk = vec![0_u8; SCAN_CHUNK_BYTES];
896 loop {
897 let read = reader
898 .read(&mut chunk)
899 .map_err(|error| CcBypassReason::InputRead {
900 path: path.to_path_buf(),
901 message: error.to_string(),
902 })?;
903 if read == 0 {
904 return Ok(false);
905 }
906 window.extend_from_slice(&chunk[..read]);
907 if needles.iter().any(|needle| {
911 memchr::memchr2_iter(needle[1], needle[1].to_ascii_uppercase(), &window).any(|offset| {
912 offset.checked_sub(1).is_some_and(|start| {
913 window[start..]
914 .get(..needle.len())
915 .is_some_and(|candidate| candidate.eq_ignore_ascii_case(needle))
916 })
917 })
918 }) {
919 return Ok(true);
920 }
921 let keep = window.len().saturating_sub(longest.saturating_sub(1));
922 window.drain(..keep);
923 }
924}
925
926#[cfg(test)]
927#[path = "depfile_tests.rs"]
928mod tests;
929
930#[cfg(test)]
931mod plain_depfile_tests {
932 use super::*;
933
934 #[test]
935 fn plain_paths_preserve_whitespace_unicode_and_make_escapes() {
936 for input in [
937 "",
938 " \t ",
939 "src/main.c include/header.h",
940 "\t路径/é.c a#b.h\t",
941 "a\u{a0}b.h",
942 r"a\ b.h a\#b.h a$$b.h",
943 r"bad\q.h",
944 "bad$",
945 "bad\\",
946 ] {
947 let expected = split_escaped_prerequisites(input);
948 let actual = split_prerequisites(input);
949 match (actual, expected) {
950 (Ok(actual), Ok(expected)) => assert_eq!(actual, expected, "{input}"),
951 (Err(actual), Err(expected)) => {
952 assert_eq!(format!("{actual:?}"), format!("{expected:?}"))
953 }
954 results => panic!("mismatched results for {input}: {results:?}"),
955 }
956 }
957 }
958
959 #[test]
960 #[ignore = "manual performance measurement; run in release mode"]
961 fn benchmark_plain_depfile() {
962 for count in [256, 4096] {
963 for escaped in [false, true] {
964 let prerequisites = (0..count)
965 .map(|index| {
966 format!(
967 " /workspace/{}/include/library-{index}/header-{index}.h",
968 if escaped {
969 r"some\ directory"
970 } else {
971 "some-directory"
972 }
973 )
974 })
975 .collect::<String>();
976 let mut before = Vec::new();
977 let mut after = Vec::new();
978 for round in 0..15 {
979 for optimized in if round % 2 == 0 {
980 [false, true]
981 } else {
982 [true, false]
983 } {
984 let start = std::time::Instant::now();
985 for _ in 0..50 {
986 let result = if optimized {
987 split_prerequisites(std::hint::black_box(&prerequisites))
988 } else {
989 split_escaped_prerequisites(std::hint::black_box(&prerequisites))
990 };
991 std::hint::black_box(result.unwrap());
992 }
993 let sample = start.elapsed().as_secs_f64() * 1e6 / 50.0;
994 if optimized {
995 after.push(sample);
996 } else {
997 before.push(sample);
998 }
999 }
1000 }
1001 before.sort_by(f64::total_cmp);
1002 after.sort_by(f64::total_cmp);
1003 eprintln!(
1004 "{count} paths, escaped={escaped}: before {:.3} us, after {:.3} us",
1005 before[7], after[7]
1006 );
1007 }
1008 }
1009 }
1010}