foundry_compilers/
cache.rs

1//! Support for compiling contracts.
2
3use crate::{
4    buildinfo::RawBuildInfo,
5    compilers::{Compiler, CompilerSettings, Language},
6    output::Builds,
7    resolver::GraphEdges,
8    ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Graph, OutputContext, Project,
9    ProjectPaths, ProjectPathsConfig, SourceCompilationKind,
10};
11use foundry_compilers_artifacts::{
12    sources::{Source, Sources},
13    Settings,
14};
15use foundry_compilers_core::{
16    error::{Result, SolcError},
17    utils::{self, strip_prefix},
18};
19use semver::Version;
20use serde::{de::DeserializeOwned, Deserialize, Serialize};
21use std::{
22    collections::{btree_map::BTreeMap, hash_map, BTreeSet, HashMap, HashSet},
23    fs,
24    path::{Path, PathBuf},
25    time::{Duration, UNIX_EPOCH},
26};
27
28/// ethers-rs format version
29///
30/// `ethers-solc` uses a different format version id, but the actual format is consistent with
31/// hardhat This allows ethers-solc to detect if the cache file was written by hardhat or
32/// `ethers-solc`
33const ETHERS_FORMAT_VERSION: &str = "ethers-rs-sol-cache-4";
34
35/// The file name of the default cache file
36pub const SOLIDITY_FILES_CACHE_FILENAME: &str = "solidity-files-cache.json";
37
38/// A multi version cache file
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct CompilerCache<S = Settings> {
41    #[serde(rename = "_format")]
42    pub format: String,
43    /// contains all directories used for the project
44    pub paths: ProjectPaths,
45    pub files: BTreeMap<PathBuf, CacheEntry>,
46    pub builds: BTreeSet<String>,
47    pub profiles: BTreeMap<String, S>,
48}
49
50impl<S> CompilerCache<S> {
51    pub fn new(format: String, paths: ProjectPaths) -> Self {
52        Self {
53            format,
54            paths,
55            files: Default::default(),
56            builds: Default::default(),
57            profiles: Default::default(),
58        }
59    }
60}
61
62impl<S: CompilerSettings> CompilerCache<S> {
63    pub fn is_empty(&self) -> bool {
64        self.files.is_empty()
65    }
66
67    /// Removes entry for the given file
68    pub fn remove(&mut self, file: &Path) -> Option<CacheEntry> {
69        self.files.remove(file)
70    }
71
72    /// How many entries the cache contains where each entry represents a sourc file
73    pub fn len(&self) -> usize {
74        self.files.len()
75    }
76
77    /// How many `Artifacts` this cache references, where a source file can have multiple artifacts
78    pub fn artifacts_len(&self) -> usize {
79        self.entries().map(|entry| entry.artifacts().count()).sum()
80    }
81
82    /// Returns an iterator over all `CacheEntry` this cache contains
83    pub fn entries(&self) -> impl Iterator<Item = &CacheEntry> {
84        self.files.values()
85    }
86
87    /// Returns the corresponding `CacheEntry` for the file if it exists
88    pub fn entry(&self, file: &Path) -> Option<&CacheEntry> {
89        self.files.get(file)
90    }
91
92    /// Returns the corresponding `CacheEntry` for the file if it exists
93    pub fn entry_mut(&mut self, file: &Path) -> Option<&mut CacheEntry> {
94        self.files.get_mut(file)
95    }
96
97    /// Reads the cache json file from the given path
98    ///
99    /// See also [`Self::read_joined()`]
100    ///
101    /// # Errors
102    ///
103    /// If the cache file does not exist
104    ///
105    /// # Examples
106    /// ```no_run
107    /// use foundry_compilers::{cache::CompilerCache, solc::SolcSettings, Project};
108    ///
109    /// let project = Project::builder().build(Default::default())?;
110    /// let mut cache = CompilerCache::<SolcSettings>::read(project.cache_path())?;
111    /// cache.join_artifacts_files(project.artifacts_path());
112    /// # Ok::<_, Box<dyn std::error::Error>>(())
113    /// ```
114    #[instrument(skip_all, name = "sol-files-cache::read")]
115    pub fn read(path: &Path) -> Result<Self> {
116        trace!("reading solfiles cache at {}", path.display());
117        let cache: Self = utils::read_json_file(path)?;
118        trace!("read cache \"{}\" with {} entries", cache.format, cache.files.len());
119        Ok(cache)
120    }
121
122    /// Reads the cache json file from the given path and returns the cache with paths adjoined to
123    /// the `ProjectPathsConfig`.
124    ///
125    /// This expects the `artifact` files to be relative to the artifacts dir of the `paths` and the
126    /// `CachEntry` paths to be relative to the root dir of the `paths`
127    ///
128    ///
129    ///
130    /// # Examples
131    /// ```no_run
132    /// use foundry_compilers::{cache::CompilerCache, solc::SolcSettings, Project};
133    ///
134    /// let project = Project::builder().build(Default::default())?;
135    /// let cache: CompilerCache<SolcSettings> = CompilerCache::read_joined(&project.paths)?;
136    /// # Ok::<_, Box<dyn std::error::Error>>(())
137    /// ```
138    pub fn read_joined<L>(paths: &ProjectPathsConfig<L>) -> Result<Self> {
139        let mut cache = Self::read(&paths.cache)?;
140        cache.join_entries(&paths.root).join_artifacts_files(&paths.artifacts);
141        Ok(cache)
142    }
143
144    /// Write the cache as json file to the given path
145    pub fn write(&self, path: &Path) -> Result<()> {
146        trace!("writing cache with {} entries to json file: \"{}\"", self.len(), path.display());
147        utils::create_parent_dir_all(path)?;
148        utils::write_json_file(self, path, 128 * 1024)?;
149        trace!("cache file located: \"{}\"", path.display());
150        Ok(())
151    }
152
153    /// Removes build infos which don't have any artifacts linked to them.
154    pub fn remove_outdated_builds(&mut self) {
155        let mut outdated = Vec::new();
156        for build_id in &self.builds {
157            if !self
158                .entries()
159                .flat_map(|e| e.artifacts.values())
160                .flat_map(|a| a.values())
161                .flat_map(|a| a.values())
162                .any(|a| a.build_id == *build_id)
163            {
164                outdated.push(build_id.to_owned());
165            }
166        }
167
168        for build_id in outdated {
169            self.builds.remove(&build_id);
170            let path = self.paths.build_infos.join(build_id).with_extension("json");
171            let _ = std::fs::remove_file(path);
172        }
173    }
174
175    /// Sets the `CacheEntry`'s file paths to `root` adjoined to `self.file`.
176    pub fn join_entries(&mut self, root: &Path) -> &mut Self {
177        self.files = std::mem::take(&mut self.files)
178            .into_iter()
179            .map(|(path, entry)| (root.join(path), entry))
180            .collect();
181        self
182    }
183
184    /// Removes `base` from all `CacheEntry` paths
185    pub fn strip_entries_prefix(&mut self, base: &Path) -> &mut Self {
186        self.files = std::mem::take(&mut self.files)
187            .into_iter()
188            .map(|(path, entry)| (path.strip_prefix(base).map(Into::into).unwrap_or(path), entry))
189            .collect();
190        self
191    }
192
193    /// Sets the artifact files location to `base` adjoined to the `CachEntries` artifacts.
194    pub fn join_artifacts_files(&mut self, base: &Path) -> &mut Self {
195        self.files.values_mut().for_each(|entry| entry.join_artifacts_files(base));
196        self
197    }
198
199    /// Removes `base` from all artifact file paths
200    pub fn strip_artifact_files_prefixes(&mut self, base: &Path) -> &mut Self {
201        self.files.values_mut().for_each(|entry| entry.strip_artifact_files_prefixes(base));
202        self
203    }
204
205    /// Removes all `CacheEntry` which source files don't exist on disk
206    ///
207    /// **NOTE:** this assumes the `files` are absolute
208    pub fn remove_missing_files(&mut self) {
209        trace!("remove non existing files from cache");
210        self.files.retain(|file, _| {
211            let exists = file.exists();
212            if !exists {
213                trace!("remove {} from cache", file.display());
214            }
215            exists
216        })
217    }
218
219    /// Checks if all artifact files exist
220    pub fn all_artifacts_exist(&self) -> bool {
221        self.files.values().all(|entry| entry.all_artifacts_exist())
222    }
223
224    /// Strips the given prefix from all `file` paths that identify a `CacheEntry` to make them
225    /// relative to the given `base` argument
226    ///
227    /// In other words this sets the keys (the file path of a solidity file) relative to the `base`
228    /// argument, so that the key `/Users/me/project/src/Greeter.sol` will be changed to
229    /// `src/Greeter.sol` if `base` is `/Users/me/project`
230    ///
231    /// # Examples
232    /// ```no_run
233    /// use foundry_compilers::{
234    ///     artifacts::contract::CompactContract, cache::CompilerCache, solc::SolcSettings, Project,
235    /// };
236    ///
237    /// let project = Project::builder().build(Default::default())?;
238    /// let cache: CompilerCache<SolcSettings> =
239    ///     CompilerCache::read(project.cache_path())?.with_stripped_file_prefixes(project.root());
240    /// let artifact: CompactContract = cache.read_artifact("src/Greeter.sol".as_ref(), "Greeter")?;
241    /// # Ok::<_, Box<dyn std::error::Error>>(())
242    /// ```
243    ///
244    /// **Note:** this only affects the source files, see [`Self::strip_artifact_files_prefixes()`]
245    pub fn with_stripped_file_prefixes(mut self, base: &Path) -> Self {
246        self.files = self
247            .files
248            .into_iter()
249            .map(|(f, e)| (utils::source_name(&f, base).to_path_buf(), e))
250            .collect();
251        self
252    }
253
254    /// Returns the path to the artifact of the given `(file, contract)` pair
255    ///
256    /// # Examples
257    /// ```no_run
258    /// use foundry_compilers::{cache::CompilerCache, solc::SolcSettings, Project};
259    ///
260    /// let project = Project::builder().build(Default::default())?;
261    /// let cache: CompilerCache<SolcSettings> = CompilerCache::read_joined(&project.paths)?;
262    /// cache.find_artifact_path("/Users/git/myproject/src/Greeter.sol".as_ref(), "Greeter");
263    /// # Ok::<_, Box<dyn std::error::Error>>(())
264    /// ```
265    pub fn find_artifact_path(&self, contract_file: &Path, contract_name: &str) -> Option<&Path> {
266        let entry = self.entry(contract_file)?;
267        entry.find_artifact_path(contract_name)
268    }
269
270    /// Finds the path to the artifact of the given `(file, contract)` pair (see
271    /// [`Self::find_artifact_path()`]) and deserializes the artifact file as JSON.
272    ///
273    /// # Examples
274    /// ```no_run
275    /// use foundry_compilers::{
276    ///     artifacts::contract::CompactContract, cache::CompilerCache, solc::SolcSettings, Project,
277    /// };
278    ///
279    /// let project = Project::builder().build(Default::default())?;
280    /// let cache = CompilerCache::<SolcSettings>::read_joined(&project.paths)?;
281    /// let artifact: CompactContract =
282    ///     cache.read_artifact("/Users/git/myproject/src/Greeter.sol".as_ref(), "Greeter")?;
283    /// # Ok::<_, Box<dyn std::error::Error>>(())
284    /// ```
285    ///
286    /// **NOTE**: unless the cache's `files` keys were modified `contract_file` is expected to be
287    /// absolute.
288    pub fn read_artifact<Artifact: DeserializeOwned>(
289        &self,
290        contract_file: &Path,
291        contract_name: &str,
292    ) -> Result<Artifact> {
293        let artifact_path =
294            self.find_artifact_path(contract_file, contract_name).ok_or_else(|| {
295                SolcError::ArtifactNotFound(contract_file.to_path_buf(), contract_name.to_string())
296            })?;
297        utils::read_json_file(artifact_path)
298    }
299
300    /// Reads all cached artifacts from disk using the given ArtifactOutput handler
301    ///
302    /// # Examples
303    /// ```no_run
304    /// use foundry_compilers::{
305    ///     artifacts::contract::CompactContractBytecode, cache::CompilerCache, solc::SolcSettings,
306    ///     Project,
307    /// };
308    ///
309    /// let project = Project::builder().build(Default::default())?;
310    /// let cache: CompilerCache<SolcSettings> = CompilerCache::read_joined(&project.paths)?;
311    /// let artifacts = cache.read_artifacts::<CompactContractBytecode>()?;
312    /// # Ok::<_, Box<dyn std::error::Error>>(())
313    /// ```
314    pub fn read_artifacts<Artifact: DeserializeOwned + Send + Sync>(
315        &self,
316    ) -> Result<Artifacts<Artifact>> {
317        use rayon::prelude::*;
318
319        let artifacts = self
320            .files
321            .par_iter()
322            .map(|(file, entry)| entry.read_artifact_files().map(|files| (file.clone(), files)))
323            .collect::<Result<ArtifactsMap<_>>>()?;
324        Ok(Artifacts(artifacts))
325    }
326
327    /// Reads all cached [BuildContext]s from disk. [BuildContext] is inlined into [RawBuildInfo]
328    /// objects, so we are basically just partially deserializing build infos here.
329    ///
330    /// [BuildContext]: crate::buildinfo::BuildContext
331    pub fn read_builds<L: Language>(&self, build_info_dir: &Path) -> Result<Builds<L>> {
332        use rayon::prelude::*;
333
334        self.builds
335            .par_iter()
336            .map(|build_id| {
337                utils::read_json_file(&build_info_dir.join(build_id).with_extension("json"))
338                    .map(|b| (build_id.clone(), b))
339            })
340            .collect::<Result<_>>()
341            .map(|b| Builds(b))
342    }
343}
344
345#[cfg(feature = "async")]
346impl<S: CompilerSettings> CompilerCache<S> {
347    pub async fn async_read(path: &Path) -> Result<Self> {
348        let path = path.to_owned();
349        Self::asyncify(move || Self::read(&path)).await
350    }
351
352    pub async fn async_write(&self, path: &Path) -> Result<()> {
353        let content = serde_json::to_vec(self)?;
354        tokio::fs::write(path, content).await.map_err(|err| SolcError::io(err, path))
355    }
356
357    async fn asyncify<F, T>(f: F) -> Result<T>
358    where
359        F: FnOnce() -> Result<T> + Send + 'static,
360        T: Send + 'static,
361    {
362        match tokio::task::spawn_blocking(f).await {
363            Ok(res) => res,
364            Err(_) => Err(SolcError::io(
365                std::io::Error::new(std::io::ErrorKind::Other, "background task failed"),
366                "",
367            )),
368        }
369    }
370}
371
372impl<S> Default for CompilerCache<S> {
373    fn default() -> Self {
374        Self {
375            format: ETHERS_FORMAT_VERSION.to_string(),
376            builds: Default::default(),
377            files: Default::default(),
378            paths: Default::default(),
379            profiles: Default::default(),
380        }
381    }
382}
383
384impl<'a, S: CompilerSettings> From<&'a ProjectPathsConfig> for CompilerCache<S> {
385    fn from(config: &'a ProjectPathsConfig) -> Self {
386        let paths = config.paths_relative();
387        Self::new(Default::default(), paths)
388    }
389}
390
391/// Cached artifact data.
392#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
393pub struct CachedArtifact {
394    /// Path to the artifact file.
395    pub path: PathBuf,
396    /// Build id which produced the given artifact.
397    pub build_id: String,
398}
399
400pub type CachedArtifacts = BTreeMap<String, BTreeMap<Version, BTreeMap<String, CachedArtifact>>>;
401
402/// A `CacheEntry` in the cache file represents a solidity file
403///
404/// A solidity file can contain several contracts, for every contract a separate `Artifact` is
405/// emitted. so the `CacheEntry` tracks the artifacts by name. A file can be compiled with multiple
406/// `solc` versions generating version specific artifacts.
407#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct CacheEntry {
410    /// the last modification time of this file
411    pub last_modification_date: u64,
412    /// hash to identify whether the content of the file changed
413    pub content_hash: String,
414    /// identifier name see [`foundry_compilers_core::utils::source_name()`]
415    pub source_name: PathBuf,
416    /// fully resolved imports of the file
417    ///
418    /// all paths start relative from the project's root: `src/importedFile.sol`
419    pub imports: BTreeSet<PathBuf>,
420    /// The solidity version pragma
421    pub version_requirement: Option<String>,
422    /// all artifacts produced for this file
423    ///
424    /// In theory a file can be compiled by different solc versions:
425    /// `A(<=0.8.10) imports C(>0.4.0)` and `B(0.8.11) imports C(>0.4.0)`
426    /// file `C` would be compiled twice, with `0.8.10` and `0.8.11`, producing two different
427    /// artifacts.
428    ///
429    /// This map tracks the artifacts by `name -> (Version -> profile -> PathBuf)`.
430    /// This mimics the default artifacts directory structure
431    pub artifacts: CachedArtifacts,
432    /// Whether this file was compiled at least once.
433    ///
434    /// If this is true and `artifacts` are empty, it means that given version of the file does
435    /// not produce any artifacts and it should not be compiled again.
436    ///
437    /// If this is false, then artifacts are definitely empty and it should be compiled if we may
438    /// need artifacts.
439    pub seen_by_compiler: bool,
440}
441
442impl CacheEntry {
443    /// Returns the last modified timestamp `Duration`
444    pub fn last_modified(&self) -> Duration {
445        Duration::from_millis(self.last_modification_date)
446    }
447
448    /// Returns the artifact path for the contract name.
449    ///
450    /// # Examples
451    ///
452    /// ```no_run
453    /// use foundry_compilers::cache::CacheEntry;
454    ///
455    /// # fn t(entry: CacheEntry) {
456    /// # stringify!(
457    /// let entry: CacheEntry = ...;
458    /// # );
459    /// entry.find_artifact_path("Greeter");
460    /// # }
461    /// ```
462    pub fn find_artifact_path(&self, contract_name: &str) -> Option<&Path> {
463        self.artifacts
464            .get(contract_name)?
465            .iter()
466            .next()
467            .and_then(|(_, a)| a.iter().next())
468            .map(|(_, p)| p.path.as_path())
469    }
470
471    /// Reads the last modification date from the file's metadata
472    pub fn read_last_modification_date(file: &Path) -> Result<u64> {
473        let last_modification_date = fs::metadata(file)
474            .map_err(|err| SolcError::io(err, file.to_path_buf()))?
475            .modified()
476            .map_err(|err| SolcError::io(err, file.to_path_buf()))?
477            .duration_since(UNIX_EPOCH)
478            .map_err(SolcError::msg)?
479            .as_millis() as u64;
480        Ok(last_modification_date)
481    }
482
483    /// Reads all artifact files associated with the `CacheEntry`
484    ///
485    /// **Note:** all artifact file paths should be absolute.
486    fn read_artifact_files<Artifact: DeserializeOwned>(
487        &self,
488    ) -> Result<BTreeMap<String, Vec<ArtifactFile<Artifact>>>> {
489        let mut artifacts = BTreeMap::new();
490        for (artifact_name, versioned_files) in self.artifacts.iter() {
491            let mut files = Vec::with_capacity(versioned_files.len());
492            for (version, cached_artifact) in versioned_files {
493                for (profile, cached_artifact) in cached_artifact {
494                    let artifact: Artifact = utils::read_json_file(&cached_artifact.path)?;
495                    files.push(ArtifactFile {
496                        artifact,
497                        file: cached_artifact.path.clone(),
498                        version: version.clone(),
499                        build_id: cached_artifact.build_id.clone(),
500                        profile: profile.clone(),
501                    });
502                }
503            }
504            artifacts.insert(artifact_name.clone(), files);
505        }
506        Ok(artifacts)
507    }
508
509    pub(crate) fn merge_artifacts<'a, A, I, T: 'a>(&mut self, artifacts: I)
510    where
511        I: IntoIterator<Item = (&'a String, A)>,
512        A: IntoIterator<Item = &'a ArtifactFile<T>>,
513    {
514        for (name, artifacts) in artifacts.into_iter() {
515            for artifact in artifacts {
516                self.artifacts
517                    .entry(name.clone())
518                    .or_default()
519                    .entry(artifact.version.clone())
520                    .or_default()
521                    .insert(
522                        artifact.profile.clone(),
523                        CachedArtifact {
524                            build_id: artifact.build_id.clone(),
525                            path: artifact.file.clone(),
526                        },
527                    );
528            }
529        }
530    }
531
532    /// Returns `true` if the artifacts set contains the given version
533    pub fn contains(&self, version: &Version, profile: &str) -> bool {
534        self.artifacts.values().any(|artifacts| {
535            artifacts.get(version).and_then(|artifacts| artifacts.get(profile)).is_some()
536        })
537    }
538
539    /// Iterator that yields all artifact files and their version
540    pub fn artifacts_versions(&self) -> impl Iterator<Item = (&Version, &str, &CachedArtifact)> {
541        self.artifacts
542            .values()
543            .flatten()
544            .flat_map(|(v, a)| a.iter().map(move |(p, a)| (v, p.as_str(), a)))
545    }
546
547    /// Returns the artifact file for the contract and version pair
548    pub fn find_artifact(
549        &self,
550        contract: &str,
551        version: &Version,
552        profile: &str,
553    ) -> Option<&CachedArtifact> {
554        self.artifacts
555            .get(contract)
556            .and_then(|files| files.get(version))
557            .and_then(|files| files.get(profile))
558    }
559
560    /// Iterator that yields all artifact files and their version
561    pub fn artifacts_for_version<'a>(
562        &'a self,
563        version: &'a Version,
564    ) -> impl Iterator<Item = &'a CachedArtifact> + 'a {
565        self.artifacts_versions().filter_map(move |(ver, _, file)| (ver == version).then_some(file))
566    }
567
568    /// Iterator that yields all artifact files
569    pub fn artifacts(&self) -> impl Iterator<Item = &CachedArtifact> {
570        self.artifacts.values().flat_map(BTreeMap::values).flat_map(BTreeMap::values)
571    }
572
573    /// Mutable iterator over all artifact files
574    pub fn artifacts_mut(&mut self) -> impl Iterator<Item = &mut CachedArtifact> {
575        self.artifacts.values_mut().flat_map(BTreeMap::values_mut).flat_map(BTreeMap::values_mut)
576    }
577
578    /// Checks if all artifact files exist
579    pub fn all_artifacts_exist(&self) -> bool {
580        self.artifacts().all(|a| a.path.exists())
581    }
582
583    /// Sets the artifact's paths to `base` adjoined to the artifact's `path`.
584    pub fn join_artifacts_files(&mut self, base: &Path) {
585        self.artifacts_mut().for_each(|a| a.path = base.join(&a.path))
586    }
587
588    /// Removes `base` from the artifact's path
589    pub fn strip_artifact_files_prefixes(&mut self, base: &Path) {
590        self.artifacts_mut().for_each(|a| {
591            if let Ok(rem) = a.path.strip_prefix(base) {
592                a.path = rem.to_path_buf();
593            }
594        })
595    }
596}
597
598/// Collection of source file paths mapped to versions.
599#[derive(Clone, Debug, Default)]
600pub struct GroupedSources {
601    pub inner: HashMap<PathBuf, HashSet<Version>>,
602}
603
604impl GroupedSources {
605    /// Inserts provided source and version into the collection.
606    pub fn insert(&mut self, file: PathBuf, version: Version) {
607        match self.inner.entry(file) {
608            hash_map::Entry::Occupied(mut entry) => {
609                entry.get_mut().insert(version);
610            }
611            hash_map::Entry::Vacant(entry) => {
612                entry.insert(HashSet::from([version]));
613            }
614        }
615    }
616
617    /// Returns true if the file was included with the given version.
618    pub fn contains(&self, file: &Path, version: &Version) -> bool {
619        self.inner.get(file).is_some_and(|versions| versions.contains(version))
620    }
621}
622
623/// A helper abstraction over the [`CompilerCache`] used to determine what files need to compiled
624/// and which `Artifacts` can be reused.
625#[derive(Debug)]
626pub(crate) struct ArtifactsCacheInner<
627    'a,
628    T: ArtifactOutput<CompilerContract = C::CompilerContract>,
629    C: Compiler,
630> {
631    /// The preexisting cache file.
632    pub cache: CompilerCache<C::Settings>,
633
634    /// All already existing artifacts.
635    pub cached_artifacts: Artifacts<T::Artifact>,
636
637    /// All already existing build infos.
638    pub cached_builds: Builds<C::Language>,
639
640    /// Relationship between all the files.
641    pub edges: GraphEdges<C::ParsedSource>,
642
643    /// The project.
644    pub project: &'a Project<C, T>,
645
646    /// Files that were invalidated and removed from cache.
647    /// Those are not grouped by version and purged completely.
648    pub dirty_sources: HashSet<PathBuf>,
649
650    /// Artifact+version pairs which are in scope for each solc version.
651    ///
652    /// Only those files will be included into cached artifacts list for each version.
653    pub sources_in_scope: GroupedSources,
654
655    /// The file hashes.
656    pub content_hashes: HashMap<PathBuf, String>,
657}
658
659impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
660    ArtifactsCacheInner<'_, T, C>
661{
662    /// Creates a new cache entry for the file
663    fn create_cache_entry(&mut self, file: PathBuf, source: &Source) {
664        let imports = self
665            .edges
666            .imports(&file)
667            .into_iter()
668            .map(|import| strip_prefix(import, self.project.root()).into())
669            .collect();
670
671        let entry = CacheEntry {
672            last_modification_date: CacheEntry::read_last_modification_date(&file)
673                .unwrap_or_default(),
674            content_hash: source.content_hash(),
675            source_name: strip_prefix(&file, self.project.root()).into(),
676            imports,
677            version_requirement: self.edges.version_requirement(&file).map(|v| v.to_string()),
678            // artifacts remain empty until we received the compiler output
679            artifacts: Default::default(),
680            seen_by_compiler: false,
681        };
682
683        self.cache.files.insert(file, entry);
684    }
685
686    /// Returns the set of [Source]s that need to be compiled to produce artifacts for requested
687    /// input.
688    ///
689    /// Source file may have one of the two [SourceCompilationKind]s:
690    /// 1. [SourceCompilationKind::Complete] - the file has been modified or compiled with different
691    ///    settings and its cache is invalidated. For such sources we request full data needed for
692    ///    artifact construction.
693    /// 2. [SourceCompilationKind::Optimized] - the file is not dirty, but is imported by a dirty
694    ///    file and thus will be processed by solc. For such files we don't need full data, so we
695    ///    are marking them as clean to optimize output selection later.
696    fn filter(&mut self, sources: &mut Sources, version: &Version, profile: &str) {
697        // sources that should be passed to compiler.
698        let mut compile_complete = HashSet::new();
699        let mut compile_optimized = HashSet::new();
700
701        for (file, source) in sources.iter() {
702            self.sources_in_scope.insert(file.clone(), version.clone());
703
704            // If we are missing artifact for file, compile it.
705            if self.is_missing_artifacts(file, version, profile) {
706                compile_complete.insert(file.clone());
707            }
708
709            // Ensure that we have a cache entry for all sources.
710            if !self.cache.files.contains_key(file) {
711                self.create_cache_entry(file.clone(), source);
712            }
713        }
714
715        // Prepare optimization by collecting sources which are imported by files requiring complete
716        // compilation.
717        for source in &compile_complete {
718            for import in self.edges.imports(source) {
719                if !compile_complete.contains(import) {
720                    compile_optimized.insert(import.clone());
721                }
722            }
723        }
724
725        sources.retain(|file, source| {
726            source.kind = if compile_complete.contains(file) {
727                SourceCompilationKind::Complete
728            } else if compile_optimized.contains(file) {
729                SourceCompilationKind::Optimized
730            } else {
731                return false;
732            };
733            true
734        });
735    }
736
737    /// Returns whether we are missing artifacts for the given file and version.
738    #[instrument(level = "trace", skip(self))]
739    fn is_missing_artifacts(&self, file: &Path, version: &Version, profile: &str) -> bool {
740        let Some(entry) = self.cache.entry(file) else {
741            trace!("missing cache entry");
742            return true;
743        };
744
745        // only check artifact's existence if the file generated artifacts.
746        // e.g. a solidity file consisting only of import statements (like interfaces that
747        // re-export) do not create artifacts
748        if entry.seen_by_compiler && entry.artifacts.is_empty() {
749            trace!("no artifacts");
750            return false;
751        }
752
753        if !entry.contains(version, profile) {
754            trace!("missing linked artifacts");
755            return true;
756        }
757
758        if entry.artifacts_for_version(version).any(|artifact| {
759            let missing_artifact = !self.cached_artifacts.has_artifact(&artifact.path);
760            if missing_artifact {
761                trace!("missing artifact \"{}\"", artifact.path.display());
762            }
763            missing_artifact
764        }) {
765            return true;
766        }
767
768        false
769    }
770
771    // Walks over all cache entires, detects dirty files and removes them from cache.
772    fn find_and_remove_dirty(&mut self) {
773        fn populate_dirty_files<D>(
774            file: &Path,
775            dirty_files: &mut HashSet<PathBuf>,
776            edges: &GraphEdges<D>,
777        ) {
778            for file in edges.importers(file) {
779                // If file is marked as dirty we either have already visited it or it was marked as
780                // dirty initially and will be visited at some point later.
781                if !dirty_files.contains(file) {
782                    dirty_files.insert(file.to_path_buf());
783                    populate_dirty_files(file, dirty_files, edges);
784                }
785            }
786        }
787
788        let existing_profiles = self.project.settings_profiles().collect::<BTreeMap<_, _>>();
789
790        let mut dirty_profiles = HashSet::new();
791        for (profile, settings) in &self.cache.profiles {
792            if !existing_profiles.get(profile.as_str()).is_some_and(|p| p.can_use_cached(settings))
793            {
794                trace!("dirty profile: {}", profile);
795                dirty_profiles.insert(profile.clone());
796            }
797        }
798
799        for profile in &dirty_profiles {
800            self.cache.profiles.remove(profile);
801        }
802
803        self.cache.files.retain(|_, entry| {
804            // keep entries which already had no artifacts
805            if entry.artifacts.is_empty() {
806                return true;
807            }
808            entry.artifacts.retain(|_, artifacts| {
809                artifacts.retain(|_, artifacts| {
810                    artifacts.retain(|profile, _| !dirty_profiles.contains(profile));
811                    !artifacts.is_empty()
812                });
813                !artifacts.is_empty()
814            });
815            !entry.artifacts.is_empty()
816        });
817
818        for (profile, settings) in existing_profiles {
819            if !self.cache.profiles.contains_key(profile) {
820                self.cache.profiles.insert(profile.to_string(), settings.clone());
821            }
822        }
823
824        // Iterate over existing cache entries.
825        let files = self.cache.files.keys().cloned().collect::<HashSet<_>>();
826
827        let mut sources = Sources::new();
828
829        // Read all sources, marking entries as dirty on I/O errors.
830        for file in &files {
831            let Ok(source) = Source::read(file) else {
832                self.dirty_sources.insert(file.clone());
833                continue;
834            };
835            sources.insert(file.clone(), source);
836        }
837
838        // Build a temporary graph for walking imports. We need this because `self.edges`
839        // only contains graph data for in-scope sources but we are operating on cache entries.
840        if let Ok(graph) = Graph::<C::ParsedSource>::resolve_sources(&self.project.paths, sources) {
841            let (sources, edges) = graph.into_sources();
842
843            // Calculate content hashes for later comparison.
844            self.fill_hashes(&sources);
845
846            // Pre-add all sources that are guaranteed to be dirty
847            for file in sources.keys() {
848                if self.is_dirty_impl(file) {
849                    self.dirty_sources.insert(file.clone());
850                }
851            }
852
853            // Perform DFS to find direct/indirect importers of dirty files.
854            for file in self.dirty_sources.clone().iter() {
855                populate_dirty_files(file, &mut self.dirty_sources, &edges);
856            }
857        } else {
858            // Purge all sources on graph resolution error.
859            self.dirty_sources.extend(files);
860        }
861
862        // Remove all dirty files from cache.
863        for file in &self.dirty_sources {
864            debug!("removing dirty file from cache: {}", file.display());
865            self.cache.remove(file);
866        }
867    }
868
869    fn is_dirty_impl(&self, file: &Path) -> bool {
870        let Some(hash) = self.content_hashes.get(file) else {
871            trace!("missing content hash");
872            return true;
873        };
874
875        let Some(entry) = self.cache.entry(file) else {
876            trace!("missing cache entry");
877            return true;
878        };
879
880        if entry.content_hash != *hash {
881            trace!("content hash changed");
882            return true;
883        }
884
885        // If any requested extra files are missing for any artifact, mark source as dirty to
886        // generate them
887        for artifacts in self.cached_artifacts.values() {
888            for artifacts in artifacts.values() {
889                for artifact_file in artifacts {
890                    if self.project.artifacts_handler().is_dirty(artifact_file).unwrap_or(true) {
891                        return true;
892                    }
893                }
894            }
895        }
896
897        // all things match, can be reused
898        false
899    }
900
901    /// Adds the file's hashes to the set if not set yet
902    fn fill_hashes(&mut self, sources: &Sources) {
903        for (file, source) in sources {
904            if let hash_map::Entry::Vacant(entry) = self.content_hashes.entry(file.clone()) {
905                entry.insert(source.content_hash());
906            }
907        }
908    }
909}
910
911/// Abstraction over configured caching which can be either non-existent or an already loaded cache
912#[allow(clippy::large_enum_variant)]
913#[derive(Debug)]
914pub(crate) enum ArtifactsCache<
915    'a,
916    T: ArtifactOutput<CompilerContract = C::CompilerContract>,
917    C: Compiler,
918> {
919    /// Cache nothing on disk
920    Ephemeral(GraphEdges<C::ParsedSource>, &'a Project<C, T>),
921    /// Handles the actual cached artifacts, detects artifacts that can be reused
922    Cached(ArtifactsCacheInner<'a, T, C>),
923}
924
925impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
926    ArtifactsCache<'a, T, C>
927{
928    /// Create a new cache instance with the given files
929    pub fn new(project: &'a Project<C, T>, edges: GraphEdges<C::ParsedSource>) -> Result<Self> {
930        /// Returns the [CompilerCache] to use
931        ///
932        /// Returns a new empty cache if the cache does not exist or `invalidate_cache` is set.
933        fn get_cache<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>(
934            project: &Project<C, T>,
935            invalidate_cache: bool,
936        ) -> CompilerCache<C::Settings> {
937            // the currently configured paths
938            let paths = project.paths.paths_relative();
939
940            if !invalidate_cache && project.cache_path().exists() {
941                if let Ok(cache) = CompilerCache::read_joined(&project.paths) {
942                    if cache.paths == paths {
943                        // unchanged project paths
944                        return cache;
945                    }
946                }
947            }
948
949            // new empty cache
950            CompilerCache::new(Default::default(), paths)
951        }
952
953        let cache = if project.cached {
954            // we only read the existing cache if we were able to resolve the entire graph
955            // if we failed to resolve an import we invalidate the cache so don't get any false
956            // positives
957            let invalidate_cache = !edges.unresolved_imports().is_empty();
958
959            // read the cache file if it already exists
960            let mut cache = get_cache(project, invalidate_cache);
961
962            cache.remove_missing_files();
963
964            // read all artifacts
965            let mut cached_artifacts = if project.paths.artifacts.exists() {
966                trace!("reading artifacts from cache...");
967                // if we failed to read the whole set of artifacts we use an empty set
968                let artifacts = cache.read_artifacts::<T::Artifact>().unwrap_or_default();
969                trace!("read {} artifacts from cache", artifacts.artifact_files().count());
970                artifacts
971            } else {
972                Default::default()
973            };
974
975            trace!("reading build infos from cache...");
976            let cached_builds = cache.read_builds(&project.paths.build_infos).unwrap_or_default();
977
978            // Remove artifacts for which we are missing a build info.
979            cached_artifacts.0.retain(|_, artifacts| {
980                artifacts.retain(|_, artifacts| {
981                    artifacts.retain(|artifact| cached_builds.contains_key(&artifact.build_id));
982                    !artifacts.is_empty()
983                });
984                !artifacts.is_empty()
985            });
986
987            let cache = ArtifactsCacheInner {
988                cache,
989                cached_artifacts,
990                cached_builds,
991                edges,
992                project,
993                dirty_sources: Default::default(),
994                content_hashes: Default::default(),
995                sources_in_scope: Default::default(),
996            };
997
998            ArtifactsCache::Cached(cache)
999        } else {
1000            // nothing to cache
1001            ArtifactsCache::Ephemeral(edges, project)
1002        };
1003
1004        Ok(cache)
1005    }
1006
1007    /// Returns the graph data for this project
1008    pub fn graph(&self) -> &GraphEdges<C::ParsedSource> {
1009        match self {
1010            ArtifactsCache::Ephemeral(graph, _) => graph,
1011            ArtifactsCache::Cached(inner) => &inner.edges,
1012        }
1013    }
1014
1015    #[cfg(test)]
1016    #[allow(unused)]
1017    #[doc(hidden)]
1018    // only useful for debugging for debugging purposes
1019    pub fn as_cached(&self) -> Option<&ArtifactsCacheInner<'a, T, C>> {
1020        match self {
1021            ArtifactsCache::Ephemeral(..) => None,
1022            ArtifactsCache::Cached(cached) => Some(cached),
1023        }
1024    }
1025
1026    pub fn output_ctx(&self) -> OutputContext<'_> {
1027        match self {
1028            ArtifactsCache::Ephemeral(..) => Default::default(),
1029            ArtifactsCache::Cached(inner) => OutputContext::new(&inner.cache),
1030        }
1031    }
1032
1033    pub fn project(&self) -> &'a Project<C, T> {
1034        match self {
1035            ArtifactsCache::Ephemeral(_, project) => project,
1036            ArtifactsCache::Cached(cache) => cache.project,
1037        }
1038    }
1039
1040    /// Adds the file's hashes to the set if not set yet
1041    pub fn remove_dirty_sources(&mut self) {
1042        match self {
1043            ArtifactsCache::Ephemeral(..) => {}
1044            ArtifactsCache::Cached(cache) => cache.find_and_remove_dirty(),
1045        }
1046    }
1047
1048    /// Filters out those sources that don't need to be compiled
1049    pub fn filter(&mut self, sources: &mut Sources, version: &Version, profile: &str) {
1050        match self {
1051            ArtifactsCache::Ephemeral(..) => {}
1052            ArtifactsCache::Cached(cache) => cache.filter(sources, version, profile),
1053        }
1054    }
1055
1056    /// Consumes the `Cache`, rebuilds the `SolFileCache` by merging all artifacts that were
1057    /// filtered out in the previous step (`Cache::filtered`) and the artifacts that were just
1058    /// compiled and written to disk `written_artifacts`.
1059    ///
1060    /// Returns all the _cached_ artifacts.
1061    pub fn consume<A>(
1062        self,
1063        written_artifacts: &Artifacts<A>,
1064        written_build_infos: &Vec<RawBuildInfo<C::Language>>,
1065        write_to_disk: bool,
1066    ) -> Result<(Artifacts<A>, Builds<C::Language>)>
1067    where
1068        T: ArtifactOutput<Artifact = A>,
1069    {
1070        let ArtifactsCache::Cached(cache) = self else {
1071            trace!("no cache configured, ephemeral");
1072            return Ok(Default::default());
1073        };
1074
1075        let ArtifactsCacheInner {
1076            mut cache,
1077            mut cached_artifacts,
1078            cached_builds,
1079            dirty_sources,
1080            sources_in_scope,
1081            project,
1082            ..
1083        } = cache;
1084
1085        // Remove cached artifacts which are out of scope, dirty or appear in `written_artifacts`.
1086        cached_artifacts.0.retain(|file, artifacts| {
1087            let file = Path::new(file);
1088            artifacts.retain(|name, artifacts| {
1089                artifacts.retain(|artifact| {
1090                    let version = &artifact.version;
1091
1092                    if !sources_in_scope.contains(file, version) {
1093                        return false;
1094                    }
1095                    if dirty_sources.contains(file) {
1096                        return false;
1097                    }
1098                    if written_artifacts.find_artifact(file, name, version).is_some() {
1099                        return false;
1100                    }
1101                    true
1102                });
1103                !artifacts.is_empty()
1104            });
1105            !artifacts.is_empty()
1106        });
1107
1108        // Update cache entries with newly written artifacts. We update data for any artifacts as
1109        // `written_artifacts` always contain the most recent data.
1110        for (file, artifacts) in written_artifacts.as_ref() {
1111            let file_path = Path::new(file);
1112            // Only update data for existing entries, we should have entries for all in-scope files
1113            // by now.
1114            if let Some(entry) = cache.files.get_mut(file_path) {
1115                entry.merge_artifacts(artifacts);
1116            }
1117        }
1118
1119        for build_info in written_build_infos {
1120            cache.builds.insert(build_info.id.clone());
1121        }
1122
1123        // write to disk
1124        if write_to_disk {
1125            cache.remove_outdated_builds();
1126            // make all `CacheEntry` paths relative to the project root and all artifact
1127            // paths relative to the artifact's directory
1128            cache
1129                .strip_entries_prefix(project.root())
1130                .strip_artifact_files_prefixes(project.artifacts_path());
1131            cache.write(project.cache_path())?;
1132        }
1133
1134        Ok((cached_artifacts, cached_builds))
1135    }
1136
1137    /// Marks the cached entry as seen by the compiler, if it's cached.
1138    pub fn compiler_seen(&mut self, file: &Path) {
1139        if let ArtifactsCache::Cached(cache) = self {
1140            if let Some(entry) = cache.cache.entry_mut(file) {
1141                entry.seen_by_compiler = true;
1142            }
1143        }
1144    }
1145}