988 lines
38 KiB
Rust
988 lines
38 KiB
Rust
//! Support for compiling contracts
|
|
use crate::{
|
|
artifacts::Sources,
|
|
config::{ProjectPaths, SolcConfig},
|
|
error::{Result, SolcError},
|
|
filter::{FilteredSource, FilteredSourceInfo, FilteredSources},
|
|
resolver::GraphEdges,
|
|
utils, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Project, ProjectPathsConfig,
|
|
Source,
|
|
};
|
|
use semver::Version;
|
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|
use std::{
|
|
collections::{
|
|
btree_map::{BTreeMap, Entry},
|
|
hash_map, BTreeSet, HashMap, HashSet,
|
|
},
|
|
fs::{self},
|
|
path::{Path, PathBuf},
|
|
time::{Duration, UNIX_EPOCH},
|
|
};
|
|
|
|
/// ethers-rs format version
|
|
///
|
|
/// `ethers-solc` uses a different format version id, but the actual format is consistent with
|
|
/// hardhat This allows ethers-solc to detect if the cache file was written by hardhat or
|
|
/// `ethers-solc`
|
|
const ETHERS_FORMAT_VERSION: &str = "ethers-rs-sol-cache-3";
|
|
|
|
/// The file name of the default cache file
|
|
pub const SOLIDITY_FILES_CACHE_FILENAME: &str = "solidity-files-cache.json";
|
|
|
|
/// A multi version cache file
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
|
pub struct SolFilesCache {
|
|
#[serde(rename = "_format")]
|
|
pub format: String,
|
|
/// contains all directories used for the project
|
|
pub paths: ProjectPaths,
|
|
pub files: BTreeMap<PathBuf, CacheEntry>,
|
|
}
|
|
|
|
impl SolFilesCache {
|
|
/// Create a new cache instance with the given files
|
|
pub fn new(files: BTreeMap<PathBuf, CacheEntry>, paths: ProjectPaths) -> Self {
|
|
Self { format: ETHERS_FORMAT_VERSION.to_string(), files, paths }
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.files.is_empty()
|
|
}
|
|
|
|
/// How many entries the cache contains where each entry represents a sourc file
|
|
pub fn len(&self) -> usize {
|
|
self.files.len()
|
|
}
|
|
|
|
/// How many `Artifacts` this cache references, where a source file can have multiple artifacts
|
|
pub fn artifacts_len(&self) -> usize {
|
|
self.entries().map(|entry| entry.artifacts().count()).sum()
|
|
}
|
|
|
|
/// Returns an iterator over all `CacheEntry` this cache contains
|
|
pub fn entries(&self) -> impl Iterator<Item = &CacheEntry> {
|
|
self.files.values()
|
|
}
|
|
|
|
/// Returns the corresponding `CacheEntry` for the file if it exists
|
|
pub fn entry(&self, file: impl AsRef<Path>) -> Option<&CacheEntry> {
|
|
self.files.get(file.as_ref())
|
|
}
|
|
|
|
/// Returns the corresponding `CacheEntry` for the file if it exists
|
|
pub fn entry_mut(&mut self, file: impl AsRef<Path>) -> Option<&mut CacheEntry> {
|
|
self.files.get_mut(file.as_ref())
|
|
}
|
|
|
|
/// Reads the cache json file from the given path
|
|
///
|
|
/// See also [`Self::read_joined()`]
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// If the cache file does not exist
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # fn t() {
|
|
/// use ethers_solc::cache::SolFilesCache;
|
|
/// use ethers_solc::Project;
|
|
///
|
|
/// let project = Project::builder().build().unwrap();
|
|
/// let mut cache = SolFilesCache::read(project.cache_path()).unwrap();
|
|
/// cache.join_artifacts_files(project.artifacts_path());
|
|
/// # }
|
|
/// ```
|
|
#[tracing::instrument(skip_all, name = "sol-files-cache::read")]
|
|
pub fn read(path: impl AsRef<Path>) -> Result<Self> {
|
|
let path = path.as_ref();
|
|
tracing::trace!("reading solfiles cache at {}", path.display());
|
|
let cache: SolFilesCache = utils::read_json_file(path)?;
|
|
tracing::trace!("read cache \"{}\" with {} entries", cache.format, cache.files.len());
|
|
Ok(cache)
|
|
}
|
|
|
|
/// Reads the cache json file from the given path and returns the cache with paths adjoined to
|
|
/// the `ProjectPathsConfig`.
|
|
///
|
|
/// This expects the `artifact` files to be relative to the artifacts dir of the `paths` and the
|
|
/// `CachEntry` paths to be relative to the root dir of the `paths`
|
|
///
|
|
///
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # fn t() {
|
|
/// use ethers_solc::cache::SolFilesCache;
|
|
/// use ethers_solc::Project;
|
|
///
|
|
/// let project = Project::builder().build().unwrap();
|
|
/// let cache = SolFilesCache::read_joined(&project.paths).unwrap();
|
|
/// # }
|
|
/// ```
|
|
pub fn read_joined(paths: &ProjectPathsConfig) -> Result<Self> {
|
|
let mut cache = SolFilesCache::read(&paths.cache)?;
|
|
cache.join_entries(&paths.root).join_artifacts_files(&paths.artifacts);
|
|
Ok(cache)
|
|
}
|
|
|
|
/// Write the cache as json file to the given path
|
|
pub fn write(&self, path: impl AsRef<Path>) -> Result<()> {
|
|
let path = path.as_ref();
|
|
utils::create_parent_dir_all(path)?;
|
|
let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
|
|
tracing::trace!(
|
|
"writing cache with {} entries to json file: \"{}\"",
|
|
self.len(),
|
|
path.display()
|
|
);
|
|
serde_json::to_writer_pretty(file, self)?;
|
|
tracing::trace!("cache file located: \"{}\"", path.display());
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets the `CacheEntry`'s file paths to `root` adjoined to `self.file`.
|
|
pub fn join_entries(&mut self, root: impl AsRef<Path>) -> &mut Self {
|
|
let root = root.as_ref();
|
|
self.files = std::mem::take(&mut self.files)
|
|
.into_iter()
|
|
.map(|(path, entry)| (root.join(path), entry))
|
|
.collect();
|
|
self
|
|
}
|
|
|
|
/// Removes `base` from all `CacheEntry` paths
|
|
pub fn strip_entries_prefix(&mut self, base: impl AsRef<Path>) -> &mut Self {
|
|
let base = base.as_ref();
|
|
self.files = std::mem::take(&mut self.files)
|
|
.into_iter()
|
|
.map(|(path, entry)| (path.strip_prefix(base).map(Into::into).unwrap_or(path), entry))
|
|
.collect();
|
|
self
|
|
}
|
|
|
|
/// Sets the artifact files location to `base` adjoined to the `CachEntries` artifacts.
|
|
pub fn join_artifacts_files(&mut self, base: impl AsRef<Path>) -> &mut Self {
|
|
let base = base.as_ref();
|
|
self.files.values_mut().for_each(|entry| entry.join_artifacts_files(base));
|
|
self
|
|
}
|
|
|
|
/// Removes `base` from all artifact file paths
|
|
pub fn strip_artifact_files_prefixes(&mut self, base: impl AsRef<Path>) -> &mut Self {
|
|
let base = base.as_ref();
|
|
self.files.values_mut().for_each(|entry| entry.strip_artifact_files_prefixes(base));
|
|
self
|
|
}
|
|
|
|
/// Removes all `CacheEntry` which source files don't exist on disk
|
|
///
|
|
/// **NOTE:** this assumes the `files` are absolute
|
|
pub fn remove_missing_files(&mut self) {
|
|
tracing::trace!("remove non existing files from cache");
|
|
self.files.retain(|file, _| {
|
|
let exists = file.exists();
|
|
if !exists {
|
|
tracing::trace!("remove {} from cache", file.display());
|
|
}
|
|
exists
|
|
})
|
|
}
|
|
|
|
/// Checks if all artifact files exist
|
|
pub fn all_artifacts_exist(&self) -> bool {
|
|
self.files.values().all(|entry| entry.all_artifacts_exist())
|
|
}
|
|
|
|
/// Strips the given prefix from all `file` paths that identify a `CacheEntry` to make them
|
|
/// relative to the given `base` argument
|
|
///
|
|
/// In other words this sets the keys (the file path of a solidity file) relative to the `base`
|
|
/// argument, so that the key `/Users/me/project/src/Greeter.sol` will be changed to
|
|
/// `src/Greeter.sol` if `base` is `/Users/me/project`
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # fn t() {
|
|
/// use ethers_solc::artifacts::contract::CompactContract;
|
|
/// use ethers_solc::cache::SolFilesCache;
|
|
/// use ethers_solc::Project;
|
|
/// let project = Project::builder().build().unwrap();
|
|
/// let cache = SolFilesCache::read(project.cache_path())
|
|
/// .unwrap()
|
|
/// .with_stripped_file_prefixes(project.root());
|
|
/// let artifact: CompactContract = cache.read_artifact("src/Greeter.sol", "Greeter").unwrap();
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// **Note:** this only affects the source files, see [`Self::strip_artifact_files_prefixes()`]
|
|
pub fn with_stripped_file_prefixes(mut self, base: impl AsRef<Path>) -> Self {
|
|
let base = base.as_ref();
|
|
self.files = self
|
|
.files
|
|
.into_iter()
|
|
.map(|(f, e)| (utils::source_name(&f, base).to_path_buf(), e))
|
|
.collect();
|
|
self
|
|
}
|
|
|
|
/// Returns the path to the artifact of the given `(file, contract)` pair
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # fn t() {
|
|
/// use ethers_solc::cache::SolFilesCache;
|
|
/// use ethers_solc::Project;
|
|
///
|
|
/// let project = Project::builder().build().unwrap();
|
|
/// let cache = SolFilesCache::read_joined(&project.paths).unwrap();
|
|
/// cache.find_artifact_path("/Users/git/myproject/src/Greeter.sol", "Greeter");
|
|
/// # }
|
|
/// ```
|
|
pub fn find_artifact_path(
|
|
&self,
|
|
contract_file: impl AsRef<Path>,
|
|
contract_name: impl AsRef<str>,
|
|
) -> Option<&PathBuf> {
|
|
let entry = self.entry(contract_file)?;
|
|
entry.find_artifact_path(contract_name)
|
|
}
|
|
|
|
/// Finds the path to the artifact of the given `(file, contract)` pair, see
|
|
/// [`Self::find_artifact_path()`], and reads the artifact as json file
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// fn t() {
|
|
/// use ethers_solc::cache::SolFilesCache;
|
|
/// use ethers_solc::Project;
|
|
/// use ethers_solc::artifacts::contract::CompactContract;
|
|
///
|
|
/// let project = Project::builder().build().unwrap();
|
|
/// let cache = SolFilesCache::read_joined(&project.paths).unwrap();
|
|
/// let artifact: CompactContract = cache.read_artifact("/Users/git/myproject/src/Greeter.sol", "Greeter").unwrap();
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// **NOTE**: unless the cache's `files` keys were modified `contract_file` is expected to be
|
|
/// absolute, see [``]
|
|
pub fn read_artifact<Artifact: DeserializeOwned>(
|
|
&self,
|
|
contract_file: impl AsRef<Path>,
|
|
contract_name: impl AsRef<str>,
|
|
) -> Result<Artifact> {
|
|
let contract_file = contract_file.as_ref();
|
|
let contract_name = contract_name.as_ref();
|
|
|
|
let artifact_path =
|
|
self.find_artifact_path(contract_file, contract_name).ok_or_else(|| {
|
|
SolcError::ArtifactNotFound(contract_file.to_path_buf(), contract_name.to_string())
|
|
})?;
|
|
|
|
utils::read_json_file(artifact_path)
|
|
}
|
|
|
|
/// Reads all cached artifacts from disk using the given ArtifactOutput handler
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// use ethers_solc::cache::SolFilesCache;
|
|
/// use ethers_solc::Project;
|
|
/// use ethers_solc::artifacts::contract::CompactContractBytecode;
|
|
/// # fn t() {
|
|
/// let project = Project::builder().build().unwrap();
|
|
/// let cache = SolFilesCache::read_joined(&project.paths).unwrap();
|
|
/// let artifacts = cache.read_artifacts::<CompactContractBytecode>().unwrap();
|
|
/// # }
|
|
/// ```
|
|
pub fn read_artifacts<Artifact: DeserializeOwned>(&self) -> Result<Artifacts<Artifact>> {
|
|
let mut artifacts = ArtifactsMap::new();
|
|
for (file, entry) in self.files.iter() {
|
|
let file_name = format!("{}", file.display());
|
|
artifacts.insert(file_name, entry.read_artifact_files()?);
|
|
}
|
|
Ok(Artifacts(artifacts))
|
|
}
|
|
|
|
/// Retains only the `CacheEntry` specified by the file + version combination.
|
|
///
|
|
/// In other words, only keep those cache entries with the paths (keys) that the iterator yields
|
|
/// and only keep the versions in the cache entry that the version iterator yields.
|
|
pub fn retain<'a, I, V>(&mut self, files: I)
|
|
where
|
|
I: IntoIterator<Item = (&'a Path, V)>,
|
|
V: IntoIterator<Item = &'a Version>,
|
|
{
|
|
let mut files: HashMap<_, _> = files.into_iter().collect();
|
|
|
|
self.files.retain(|file, entry| {
|
|
if entry.artifacts.is_empty() {
|
|
// keep entries that didn't emit any artifacts in the first place, such as a
|
|
// solidity file that only includes error definitions
|
|
return true
|
|
}
|
|
|
|
if let Some(versions) = files.remove(file.as_path()) {
|
|
entry.retain_versions(versions);
|
|
} else {
|
|
return false
|
|
}
|
|
!entry.artifacts.is_empty()
|
|
});
|
|
}
|
|
|
|
/// Inserts the provided cache entries, if there is an existing `CacheEntry` it will be updated
|
|
/// but versions will be merged.
|
|
pub fn extend<I>(&mut self, entries: I)
|
|
where
|
|
I: IntoIterator<Item = (PathBuf, CacheEntry)>,
|
|
{
|
|
for (file, entry) in entries.into_iter() {
|
|
match self.files.entry(file) {
|
|
Entry::Vacant(e) => {
|
|
e.insert(entry);
|
|
}
|
|
Entry::Occupied(mut other) => {
|
|
other.get_mut().merge_artifacts(entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// async variants for read and write
|
|
#[cfg(feature = "async")]
|
|
impl SolFilesCache {
|
|
pub async fn async_read(path: impl AsRef<Path>) -> Result<Self> {
|
|
let path = path.as_ref();
|
|
let content =
|
|
tokio::fs::read_to_string(path).await.map_err(|err| SolcError::io(err, path))?;
|
|
Ok(serde_json::from_str(&content)?)
|
|
}
|
|
|
|
pub async fn async_write(&self, path: impl AsRef<Path>) -> Result<()> {
|
|
let path = path.as_ref();
|
|
let content = serde_json::to_vec_pretty(self)?;
|
|
tokio::fs::write(path, content).await.map_err(|err| SolcError::io(err, path))
|
|
}
|
|
}
|
|
|
|
impl Default for SolFilesCache {
|
|
fn default() -> Self {
|
|
SolFilesCache {
|
|
format: ETHERS_FORMAT_VERSION.to_string(),
|
|
files: Default::default(),
|
|
paths: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a ProjectPathsConfig> for SolFilesCache {
|
|
fn from(config: &'a ProjectPathsConfig) -> Self {
|
|
let paths = config.paths_relative();
|
|
SolFilesCache::new(Default::default(), paths)
|
|
}
|
|
}
|
|
|
|
/// A `CacheEntry` in the cache file represents a solidity file
|
|
///
|
|
/// A solidity file can contain several contracts, for every contract a separate `Artifact` is
|
|
/// emitted. so the `CacheEntry` tracks the artifacts by name. A file can be compiled with multiple
|
|
/// `solc` versions generating version specific artifacts.
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CacheEntry {
|
|
/// the last modification time of this file
|
|
pub last_modification_date: u64,
|
|
/// hash to identify whether the content of the file changed
|
|
pub content_hash: String,
|
|
/// identifier name see [`crate::utils::source_name()`]
|
|
pub source_name: PathBuf,
|
|
/// what config was set when compiling this file
|
|
pub solc_config: SolcConfig,
|
|
/// fully resolved imports of the file
|
|
///
|
|
/// all paths start relative from the project's root: `src/importedFile.sol`
|
|
pub imports: BTreeSet<PathBuf>,
|
|
/// The solidity version pragma
|
|
pub version_requirement: Option<String>,
|
|
/// all artifacts produced for this file
|
|
///
|
|
/// In theory a file can be compiled by different solc versions:
|
|
/// `A(<=0.8.10) imports C(>0.4.0)` and `B(0.8.11) imports C(>0.4.0)`
|
|
/// file `C` would be compiled twice, with `0.8.10` and `0.8.11`, producing two different
|
|
/// artifacts.
|
|
///
|
|
/// This map tracks the artifacts by `name -> (Version -> PathBuf)`.
|
|
/// This mimics the default artifacts directory structure
|
|
pub artifacts: BTreeMap<String, BTreeMap<Version, PathBuf>>,
|
|
}
|
|
|
|
impl CacheEntry {
|
|
/// Returns the last modified timestamp `Duration`
|
|
pub fn last_modified(&self) -> Duration {
|
|
Duration::from_millis(self.last_modification_date)
|
|
}
|
|
|
|
/// Returns the artifact path for the contract name
|
|
/// ```
|
|
/// use ethers_solc::cache::CacheEntry;
|
|
/// # fn t(entry: CacheEntry) {
|
|
/// entry.find_artifact_path("Greeter");
|
|
/// # }
|
|
/// ```
|
|
pub fn find_artifact_path(&self, contract_name: impl AsRef<str>) -> Option<&PathBuf> {
|
|
self.artifacts.get(contract_name.as_ref())?.iter().next().map(|(_, p)| p)
|
|
}
|
|
|
|
/// Reads the last modification date from the file's metadata
|
|
pub fn read_last_modification_date(file: impl AsRef<Path>) -> Result<u64> {
|
|
let file = file.as_ref();
|
|
let last_modification_date = fs::metadata(file)
|
|
.map_err(|err| SolcError::io(err, file.to_path_buf()))?
|
|
.modified()
|
|
.map_err(|err| SolcError::io(err, file.to_path_buf()))?
|
|
.duration_since(UNIX_EPOCH)
|
|
.map_err(|err| SolcError::solc(err.to_string()))?
|
|
.as_millis() as u64;
|
|
Ok(last_modification_date)
|
|
}
|
|
|
|
/// Reads all artifact files associated with the `CacheEntry`
|
|
///
|
|
/// **Note:** all artifact file paths should be absolute, see [`Self::join`]
|
|
fn read_artifact_files<Artifact: DeserializeOwned>(
|
|
&self,
|
|
) -> Result<BTreeMap<String, Vec<ArtifactFile<Artifact>>>> {
|
|
let mut artifacts = BTreeMap::new();
|
|
for (artifact_name, versioned_files) in self.artifacts.iter() {
|
|
let mut files = Vec::with_capacity(versioned_files.len());
|
|
for (version, file) in versioned_files {
|
|
let artifact: Artifact = utils::read_json_file(file)?;
|
|
files.push(ArtifactFile { artifact, file: file.clone(), version: version.clone() });
|
|
}
|
|
artifacts.insert(artifact_name.clone(), files);
|
|
}
|
|
Ok(artifacts)
|
|
}
|
|
|
|
pub(crate) fn insert_artifacts<'a, I, T: 'a>(&mut self, artifacts: I)
|
|
where
|
|
I: IntoIterator<Item = (&'a String, Vec<&'a ArtifactFile<T>>)>,
|
|
{
|
|
for (name, artifacts) in artifacts.into_iter().filter(|(_, a)| !a.is_empty()) {
|
|
let entries: BTreeMap<_, _> = artifacts
|
|
.into_iter()
|
|
.map(|artifact| (artifact.version.clone(), artifact.file.clone()))
|
|
.collect();
|
|
self.artifacts.insert(name.clone(), entries);
|
|
}
|
|
}
|
|
|
|
/// Merges another `CacheEntries` artifacts into the existing set
|
|
fn merge_artifacts(&mut self, other: CacheEntry) {
|
|
for (name, artifacts) in other.artifacts {
|
|
match self.artifacts.entry(name) {
|
|
Entry::Vacant(entry) => {
|
|
entry.insert(artifacts);
|
|
}
|
|
Entry::Occupied(mut entry) => {
|
|
entry.get_mut().extend(artifacts.into_iter());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Retains only those artifacts that match the provided versions.
|
|
///
|
|
/// Removes an artifact entry if none of its versions is included in the `versions` set.
|
|
pub fn retain_versions<'a, I>(&mut self, versions: I)
|
|
where
|
|
I: IntoIterator<Item = &'a Version>,
|
|
{
|
|
let versions = versions.into_iter().collect::<HashSet<_>>();
|
|
self.artifacts.retain(|_, artifacts| {
|
|
artifacts.retain(|version, _| versions.contains(version));
|
|
!artifacts.is_empty()
|
|
})
|
|
}
|
|
|
|
/// Returns `true` if the artifacts set contains the given version
|
|
pub fn contains_version(&self, version: &Version) -> bool {
|
|
self.artifacts_versions().any(|(v, _)| v == version)
|
|
}
|
|
|
|
/// Iterator that yields all artifact files and their version
|
|
pub fn artifacts_versions(&self) -> impl Iterator<Item = (&Version, &PathBuf)> {
|
|
self.artifacts.values().flat_map(|artifacts| artifacts.iter())
|
|
}
|
|
|
|
/// Iterator that yields all artifact files and their version
|
|
pub fn artifacts_for_version<'a>(
|
|
&'a self,
|
|
version: &'a Version,
|
|
) -> impl Iterator<Item = &'a PathBuf> + 'a {
|
|
self.artifacts_versions().filter_map(move |(ver, file)| (ver == version).then_some(file))
|
|
}
|
|
|
|
/// Iterator that yields all artifact files
|
|
pub fn artifacts(&self) -> impl Iterator<Item = &PathBuf> {
|
|
self.artifacts.values().flat_map(|artifacts| artifacts.values())
|
|
}
|
|
|
|
/// Mutable iterator over all artifact files
|
|
pub fn artifacts_mut(&mut self) -> impl Iterator<Item = &mut PathBuf> {
|
|
self.artifacts.values_mut().flat_map(|artifacts| artifacts.values_mut())
|
|
}
|
|
|
|
/// Checks if all artifact files exist
|
|
pub fn all_artifacts_exist(&self) -> bool {
|
|
self.artifacts().all(|p| p.exists())
|
|
}
|
|
|
|
/// Sets the artifact's paths to `base` adjoined to the artifact's `path`.
|
|
pub fn join_artifacts_files(&mut self, base: impl AsRef<Path>) {
|
|
let base = base.as_ref();
|
|
self.artifacts_mut().for_each(|p| *p = base.join(&*p))
|
|
}
|
|
|
|
/// Removes `base` from the artifact's path
|
|
pub fn strip_artifact_files_prefixes(&mut self, base: impl AsRef<Path>) {
|
|
let base = base.as_ref();
|
|
self.artifacts_mut().for_each(|p| {
|
|
if let Ok(rem) = p.strip_prefix(base) {
|
|
*p = rem.to_path_buf();
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A helper abstraction over the [`SolFilesCache`] used to determine what files need to compiled
|
|
/// and which `Artifacts` can be reused.
|
|
#[derive(Debug)]
|
|
pub(crate) struct ArtifactsCacheInner<'a, T: ArtifactOutput> {
|
|
/// preexisting cache file
|
|
pub cache: SolFilesCache,
|
|
/// all already existing artifacts
|
|
pub cached_artifacts: Artifacts<T::Artifact>,
|
|
/// relationship between all the files
|
|
pub edges: GraphEdges,
|
|
/// the project
|
|
pub project: &'a Project<T>,
|
|
/// all files that were filtered because they haven't changed
|
|
pub filtered: HashMap<PathBuf, (Source, HashSet<Version>)>,
|
|
/// the corresponding cache entries for all sources that were deemed to be dirty
|
|
///
|
|
/// `CacheEntry` are grouped by their solidity file.
|
|
/// During preprocessing the `artifacts` field of a new `CacheEntry` is left blank, because in
|
|
/// order to determine the artifacts of the solidity file, the file needs to be compiled first.
|
|
/// Only after the `CompilerOutput` is received and all compiled contracts are handled, see
|
|
/// [`crate::ArtifactOutput::on_output()`] all artifacts, their disk paths, are determined and
|
|
/// can be populated before the updated [`crate::SolFilesCache`] is finally written to disk,
|
|
/// see [`Cache::finish()`]
|
|
pub dirty_source_files: HashMap<PathBuf, (CacheEntry, HashSet<Version>)>,
|
|
/// the file hashes
|
|
pub content_hashes: HashMap<PathBuf, String>,
|
|
}
|
|
|
|
impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
|
|
/// Creates a new cache entry for the file
|
|
fn create_cache_entry(&self, file: &Path, source: &Source) -> CacheEntry {
|
|
let imports = self
|
|
.edges
|
|
.imports(file)
|
|
.into_iter()
|
|
.map(|import| utils::source_name(import, self.project.root()).to_path_buf())
|
|
.collect();
|
|
|
|
let entry = CacheEntry {
|
|
last_modification_date: CacheEntry::read_last_modification_date(&file)
|
|
.unwrap_or_default(),
|
|
content_hash: source.content_hash(),
|
|
source_name: utils::source_name(file, self.project.root()).into(),
|
|
solc_config: self.project.solc_config.clone(),
|
|
imports,
|
|
version_requirement: self.edges.version_requirement(file).map(|v| v.to_string()),
|
|
// artifacts remain empty until we received the compiler output
|
|
artifacts: Default::default(),
|
|
};
|
|
|
|
entry
|
|
}
|
|
|
|
/// inserts a new cache entry for the given file
|
|
///
|
|
/// If there is already an entry available for the file the given version is added to the set
|
|
fn insert_new_cache_entry(&mut self, file: &Path, source: &Source, version: Version) {
|
|
if let Some((_, versions)) = self.dirty_source_files.get_mut(file) {
|
|
versions.insert(version);
|
|
} else {
|
|
let entry = self.create_cache_entry(file, source);
|
|
self.dirty_source_files.insert(file.to_path_buf(), (entry, HashSet::from([version])));
|
|
}
|
|
}
|
|
|
|
/// inserts the filtered source with the given version
|
|
fn insert_filtered_source(&mut self, file: PathBuf, source: Source, version: Version) {
|
|
match self.filtered.entry(file) {
|
|
hash_map::Entry::Occupied(mut entry) => {
|
|
entry.get_mut().1.insert(version);
|
|
}
|
|
hash_map::Entry::Vacant(entry) => {
|
|
entry.insert((source, HashSet::from([version])));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the set of [Source]s that need to be included in the [CompilerOutput] in order to
|
|
/// recompile the project.
|
|
///
|
|
/// We define _dirty_ sources as files that:
|
|
/// - are new
|
|
/// - were changed
|
|
/// - their imports were changed
|
|
/// - their artifact is missing
|
|
///
|
|
/// A _dirty_ file is always included in the [CompilerInput].
|
|
/// A _dirty_ file can also include clean files - files that do not match any of the above
|
|
/// criteria - which solc also requires in order to compile a dirty file.
|
|
///
|
|
/// Therefore, these files will also be included in the filtered output but not marked as dirty,
|
|
/// so that their [OutputSelection] can be optimized in the [CompilerOutput] and their (empty)
|
|
/// artifacts ignored.
|
|
fn filter(&mut self, sources: Sources, version: &Version) -> FilteredSources {
|
|
// all files that are not dirty themselves, but are pulled from a dirty file
|
|
let mut imports_of_dirty = HashSet::new();
|
|
|
|
// separates all source files that fit the criteria (dirty) from those that don't (clean)
|
|
let (mut filtered_sources, clean_sources) = sources
|
|
.into_iter()
|
|
.map(|(file, source)| self.filter_source(file, source, version))
|
|
.fold(
|
|
(BTreeMap::default(), Vec::new()),
|
|
|(mut dirty_sources, mut clean_sources), source| {
|
|
if source.dirty {
|
|
// mark all files that are imported by a dirty file
|
|
imports_of_dirty.extend(self.edges.all_imported_nodes(source.idx));
|
|
dirty_sources.insert(source.file, FilteredSource::Dirty(source.source));
|
|
} else {
|
|
clean_sources.push(source);
|
|
}
|
|
|
|
(dirty_sources, clean_sources)
|
|
},
|
|
);
|
|
|
|
// track new cache entries for dirty files
|
|
for (file, filtered) in filtered_sources.iter() {
|
|
self.insert_new_cache_entry(file, filtered.source(), version.clone());
|
|
}
|
|
|
|
for clean_source in clean_sources {
|
|
let FilteredSourceInfo { file, source, idx, .. } = clean_source;
|
|
if imports_of_dirty.contains(&idx) {
|
|
// file is pulled in by a dirty file
|
|
filtered_sources.insert(file.clone(), FilteredSource::Clean(source.clone()));
|
|
}
|
|
self.insert_filtered_source(file, source, version.clone());
|
|
}
|
|
|
|
filtered_sources.into()
|
|
}
|
|
|
|
/// Returns the state of the given source file.
|
|
fn filter_source(
|
|
&self,
|
|
file: PathBuf,
|
|
source: Source,
|
|
version: &Version,
|
|
) -> FilteredSourceInfo {
|
|
let idx = self.edges.node_id(&file);
|
|
if !self.is_dirty(&file, version) &&
|
|
self.edges.imports(&file).iter().all(|file| !self.is_dirty(file, version))
|
|
{
|
|
FilteredSourceInfo { file, source, idx, dirty: false }
|
|
} else {
|
|
FilteredSourceInfo { file, source, idx, dirty: true }
|
|
}
|
|
}
|
|
|
|
/// returns `false` if the corresponding cache entry remained unchanged otherwise `true`
|
|
fn is_dirty(&self, file: &Path, version: &Version) -> bool {
|
|
if let Some(hash) = self.content_hashes.get(file) {
|
|
if let Some(entry) = self.cache.entry(&file) {
|
|
if entry.content_hash.as_bytes() != hash.as_bytes() {
|
|
tracing::trace!("changed content hash for source file \"{}\"", file.display());
|
|
return true
|
|
}
|
|
if self.project.solc_config != entry.solc_config {
|
|
tracing::trace!("changed solc config for source file \"{}\"", file.display());
|
|
return true
|
|
}
|
|
|
|
// only check artifact's existence if the file generated artifacts.
|
|
// e.g. a solidity file consisting only of import statements (like interfaces that
|
|
// re-export) do not create artifacts
|
|
if !entry.artifacts.is_empty() {
|
|
if !entry.contains_version(version) {
|
|
tracing::trace!(
|
|
"missing linked artifacts for source file `{}` for version \"{}\"",
|
|
file.display(),
|
|
version
|
|
);
|
|
return true
|
|
}
|
|
|
|
if entry.artifacts_for_version(version).any(|artifact_path| {
|
|
let missing_artifact = !self.cached_artifacts.has_artifact(artifact_path);
|
|
if missing_artifact {
|
|
tracing::trace!("missing artifact \"{}\"", artifact_path.display());
|
|
}
|
|
missing_artifact
|
|
}) {
|
|
return true
|
|
}
|
|
}
|
|
// all things match, can be reused
|
|
return false
|
|
}
|
|
tracing::trace!("Missing cache entry for {}", file.display());
|
|
} else {
|
|
tracing::trace!("Missing content hash for {}", file.display());
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Adds the file's hashes to the set if not set yet
|
|
fn fill_hashes(&mut self, sources: &Sources) {
|
|
for (file, source) in sources {
|
|
if let hash_map::Entry::Vacant(entry) = self.content_hashes.entry(file.clone()) {
|
|
entry.insert(source.content_hash());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Abstraction over configured caching which can be either non-existent or an already loaded cache
|
|
#[allow(clippy::large_enum_variant)]
|
|
#[derive(Debug)]
|
|
pub(crate) enum ArtifactsCache<'a, T: ArtifactOutput> {
|
|
/// Cache nothing on disk
|
|
Ephemeral(GraphEdges, &'a Project<T>),
|
|
/// Handles the actual cached artifacts, detects artifacts that can be reused
|
|
Cached(ArtifactsCacheInner<'a, T>),
|
|
}
|
|
|
|
impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> {
|
|
pub fn new(project: &'a Project<T>, edges: GraphEdges) -> Result<Self> {
|
|
/// Returns the [SolFilesCache] to use
|
|
///
|
|
/// Returns a new empty cache if the cache does not exist or `invalidate_cache` is set.
|
|
fn get_cache<T: ArtifactOutput>(
|
|
project: &Project<T>,
|
|
invalidate_cache: bool,
|
|
) -> SolFilesCache {
|
|
// the currently configured paths
|
|
let paths = project.paths.paths_relative();
|
|
|
|
if !invalidate_cache && project.cache_path().exists() {
|
|
if let Ok(cache) = SolFilesCache::read_joined(&project.paths) {
|
|
if cache.paths == paths {
|
|
// unchanged project paths
|
|
return cache
|
|
}
|
|
}
|
|
}
|
|
|
|
// new empty cache
|
|
SolFilesCache::new(Default::default(), paths)
|
|
}
|
|
|
|
let cache = if project.cached {
|
|
// we only read the existing cache if we were able to resolve the entire graph
|
|
// if we failed to resolve an import we invalidate the cache so don't get any false
|
|
// positives
|
|
let invalidate_cache = !edges.unresolved_imports().is_empty();
|
|
|
|
// read the cache file if it already exists
|
|
let mut cache = get_cache(project, invalidate_cache);
|
|
|
|
cache.remove_missing_files();
|
|
|
|
// read all artifacts
|
|
let cached_artifacts = if project.paths.artifacts.exists() {
|
|
tracing::trace!("reading artifacts from cache...");
|
|
// if we failed to read the whole set of artifacts we use an empty set
|
|
let artifacts = cache.read_artifacts::<T::Artifact>().unwrap_or_default();
|
|
tracing::trace!("read {} artifacts from cache", artifacts.artifact_files().count());
|
|
artifacts
|
|
} else {
|
|
Default::default()
|
|
};
|
|
|
|
let cache = ArtifactsCacheInner {
|
|
cache,
|
|
cached_artifacts,
|
|
edges,
|
|
project,
|
|
filtered: Default::default(),
|
|
dirty_source_files: Default::default(),
|
|
content_hashes: Default::default(),
|
|
};
|
|
|
|
ArtifactsCache::Cached(cache)
|
|
} else {
|
|
// nothing to cache
|
|
ArtifactsCache::Ephemeral(edges, project)
|
|
};
|
|
|
|
Ok(cache)
|
|
}
|
|
|
|
/// Returns the graph data for this project
|
|
pub fn graph(&self) -> &GraphEdges {
|
|
match self {
|
|
ArtifactsCache::Ephemeral(graph, _) => graph,
|
|
ArtifactsCache::Cached(inner) => &inner.edges,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(unused)]
|
|
#[doc(hidden)]
|
|
// only useful for debugging for debugging purposes
|
|
pub fn as_cached(&self) -> Option<&ArtifactsCacheInner<'a, T>> {
|
|
match self {
|
|
ArtifactsCache::Ephemeral(_, _) => None,
|
|
ArtifactsCache::Cached(cached) => Some(cached),
|
|
}
|
|
}
|
|
|
|
pub fn project(&self) -> &'a Project<T> {
|
|
match self {
|
|
ArtifactsCache::Ephemeral(_, project) => project,
|
|
ArtifactsCache::Cached(cache) => cache.project,
|
|
}
|
|
}
|
|
|
|
/// Adds the file's hashes to the set if not set yet
|
|
pub fn fill_content_hashes(&mut self, sources: &Sources) {
|
|
match self {
|
|
ArtifactsCache::Ephemeral(_, _) => {}
|
|
ArtifactsCache::Cached(cache) => cache.fill_hashes(sources),
|
|
}
|
|
}
|
|
|
|
/// Filters out those sources that don't need to be compiled
|
|
pub fn filter(&mut self, sources: Sources, version: &Version) -> FilteredSources {
|
|
match self {
|
|
ArtifactsCache::Ephemeral(_, _) => sources.into(),
|
|
ArtifactsCache::Cached(cache) => cache.filter(sources, version),
|
|
}
|
|
}
|
|
|
|
/// Consumes the `Cache`, rebuilds the [`SolFileCache`] by merging all artifacts that were
|
|
/// filtered out in the previous step (`Cache::filtered`) and the artifacts that were just
|
|
/// compiled and written to disk `written_artifacts`.
|
|
///
|
|
/// Returns all the _cached_ artifacts.
|
|
pub fn consume(
|
|
self,
|
|
written_artifacts: &Artifacts<T::Artifact>,
|
|
write_to_disk: bool,
|
|
) -> Result<Artifacts<T::Artifact>> {
|
|
match self {
|
|
ArtifactsCache::Ephemeral(_, _) => Ok(Default::default()),
|
|
ArtifactsCache::Cached(cache) => {
|
|
let ArtifactsCacheInner {
|
|
mut cache,
|
|
mut cached_artifacts,
|
|
mut dirty_source_files,
|
|
filtered,
|
|
project,
|
|
..
|
|
} = cache;
|
|
|
|
// keep only those files that were previously filtered (not dirty, reused)
|
|
cache.retain(filtered.iter().map(|(p, (_, v))| (p.as_path(), v)));
|
|
|
|
// add the written artifacts to the cache entries, this way we can keep a mapping
|
|
// from solidity file to its artifacts
|
|
// this step is necessary because the concrete artifacts are only known after solc
|
|
// was invoked and received as output, before that we merely know the file and
|
|
// the versions, so we add the artifacts on a file by file basis
|
|
for (file, written_artifacts) in written_artifacts.as_ref() {
|
|
let file_path = Path::new(&file);
|
|
if let Some((cache_entry, versions)) = dirty_source_files.get_mut(file_path) {
|
|
cache_entry.insert_artifacts(written_artifacts.iter().map(
|
|
|(name, artifacts)| {
|
|
let artifacts = artifacts
|
|
.iter()
|
|
.filter(|artifact| versions.contains(&artifact.version))
|
|
.collect::<Vec<_>>();
|
|
(name, artifacts)
|
|
},
|
|
));
|
|
}
|
|
|
|
// cached artifacts that were overwritten also need to be removed from the
|
|
// `cached_artifacts` set
|
|
if let Some((f, mut cached)) = cached_artifacts.0.remove_entry(file) {
|
|
tracing::trace!("checking {} for obsolete cached artifact entries", file);
|
|
cached.retain(|name, cached_artifacts| {
|
|
if let Some(written_files) = written_artifacts.get(name) {
|
|
// written artifact clashes with a cached artifact, so we need to decide whether to keep or to remove the cached
|
|
cached_artifacts.retain(|f| {
|
|
// we only keep those artifacts that don't conflict with written artifacts and which version was a compiler target
|
|
let retain = written_files
|
|
.iter()
|
|
.all(|other| other.version != f.version) && filtered.get(
|
|
&PathBuf::from(file)).map(|(_, versions)| {
|
|
versions.contains(&f.version)
|
|
}).unwrap_or_default();
|
|
if !retain {
|
|
tracing::trace!(
|
|
"purging obsolete cached artifact for contract {} and version {}",
|
|
name,
|
|
f.version
|
|
);
|
|
}
|
|
retain
|
|
});
|
|
return !cached_artifacts.is_empty()
|
|
}
|
|
false
|
|
});
|
|
|
|
if !cached.is_empty() {
|
|
cached_artifacts.0.insert(f, cached);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add the new cache entries to the cache file
|
|
cache
|
|
.extend(dirty_source_files.into_iter().map(|(file, (entry, _))| (file, entry)));
|
|
|
|
// write to disk
|
|
if write_to_disk {
|
|
// make all `CacheEntry` paths relative to the project root and all artifact
|
|
// paths relative to the artifact's directory
|
|
cache
|
|
.strip_entries_prefix(project.root())
|
|
.strip_artifact_files_prefixes(project.artifacts_path());
|
|
cache.write(project.cache_path())?;
|
|
}
|
|
|
|
Ok(cached_artifacts)
|
|
}
|
|
}
|
|
}
|
|
}
|