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