Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

fix(solc): fix cache and allowed paths bug #998

Merged
merged 3 commits into from
Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 52 additions & 17 deletions ethers-solc/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
}
}

/// inserts the filtered source with the fiven 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) => {
Expand All @@ -593,35 +593,62 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
}
}

/// Returns only those sources that
/// Returns only dirty sources that:
/// - are new
/// - were changed
/// - their imports were changed
/// - their artifact is missing
/// This also includes their respective imports
fn filter(&mut self, sources: Sources, version: &Version) -> Sources {
self.fill_hashes(&sources);
sources

let mut imports_of_dirty = HashSet::new();
// separates all source files that fit the criteria (dirty) from those that don't (clean)
let (mut dirty_sources, clean_sources) = sources
.into_iter()
.filter_map(|(file, source)| self.requires_solc(file, source, version))
.collect()
.map(|(file, source)| self.filter_source(file, source, version))
.fold(
(Sources::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, source.source);
} else {
clean_sources.push(source);
}

(dirty_sources, clean_sources)
},
);

for clean_source in clean_sources {
let FilteredSource { file, source, idx, .. } = clean_source;
if imports_of_dirty.contains(&idx) {
// file is imported by a dirty file
dirty_sources.insert(file, source);
} else {
self.insert_filtered_source(file, source, version.clone());
}
}

// track dirty sources internally
for (file, source) in dirty_sources.iter() {
self.insert_new_cache_entry(file, source, version.clone());
}

dirty_sources
}

/// Returns `Some` if the file _needs_ to be compiled and `None` if the artifact can be reu-used
fn requires_solc(
&mut self,
file: PathBuf,
source: Source,
version: &Version,
) -> Option<(PathBuf, Source)> {
/// Returns the state of the given source file.
fn filter_source(&self, file: PathBuf, source: Source, version: &Version) -> FilteredSource {
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))
{
self.insert_filtered_source(file, source, version.clone());
None
FilteredSource { file, source, idx, dirty: false }
} else {
self.insert_new_cache_entry(&file, &source, version.clone());

Some((file, source))
FilteredSource { file, source, idx, dirty: true }
}
}

Expand Down Expand Up @@ -674,6 +701,14 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
}
}

/// Helper type to represent the state of a source file
struct FilteredSource {
file: PathBuf,
source: Source,
idx: usize,
dirty: bool,
}

/// Abstraction over configured caching which can be either non-existent or an already loaded cache
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
Expand Down
16 changes: 10 additions & 6 deletions ethers-solc/src/compile/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
let Self { edges, project, mut sources } = self;

let mut cache = ArtifactsCache::new(project, edges)?;
// retain and compile only dirty sources
// retain and compile only dirty sources and all their imports
sources = sources.filtered(&mut cache);

Ok(PreprocessedState { sources, cache })
Expand Down Expand Up @@ -216,18 +216,22 @@ struct CompiledState<'a, T: ArtifactOutput> {
impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
/// advance to the next state by handling all artifacts
///
/// Writes all output contracts to disk if enabled in the `Project`
/// Writes all output contracts to disk if enabled in the `Project` and if the build was
/// successful
fn write_artifacts(self) -> Result<ArtifactsState<'a, T>> {
let CompiledState { output, cache } = self;

// write all artifacts
let compiled_artifacts = if !cache.project().no_artifacts {
// write all artifacts via the handler but only if the build succeeded
let compiled_artifacts = if cache.project().no_artifacts {
cache.project().artifacts_handler().output_to_artifacts(&output.contracts)
} else if output.has_error() {
tracing::trace!("skip writing cache file due to solc errors: {:?}", output.errors);
cache.project().artifacts_handler().output_to_artifacts(&output.contracts)
} else {
cache
.project()
.artifacts_handler()
.on_output(&output.contracts, &cache.project().paths)?
} else {
cache.project().artifacts_handler().output_to_artifacts(&output.contracts)
};

Ok(ArtifactsState { output, cache, compiled_artifacts })
Expand Down
6 changes: 4 additions & 2 deletions ethers-solc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,11 @@ impl<T: ArtifactOutput> Project<T> {
&self.artifacts
}

/// Applies the configured settings to the given `Solc`
/// Applies the configured arguments to the given `Solc`
///
/// This will set the `--allow-paths` to the paths configured for the `Project`, if any.
fn configure_solc(&self, mut solc: Solc) -> Solc {
if self.allowed_lib_paths.0.is_empty() {
if solc.args.is_empty() && !self.allowed_lib_paths.0.is_empty() {
solc = solc.arg("--allow-paths").arg(self.allowed_lib_paths.to_string());
}
solc
Expand Down
10 changes: 10 additions & 0 deletions ethers-solc/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ impl GraphEdges {
&self.edges[from]
}

/// Returns an iterator that yields all imports of a node and all their imports
pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
NodesIter::new(from, self).skip(1)
}

/// Returns all files imported by the given file
pub fn imports(&self, file: impl AsRef<Path>) -> HashSet<&PathBuf> {
if let Some(start) = self.indices.get(file.as_ref()).copied() {
Expand All @@ -100,6 +105,11 @@ impl GraphEdges {
}
}

/// Returns the id of the given file
pub fn node_id(&self, file: impl AsRef<Path>) -> usize {
self.indices[file.as_ref()]
}

/// Returns true if the `file` was originally included when the graph was first created and not
/// added when all `imports` were resolved
pub fn is_input_file(&self, file: impl AsRef<Path>) -> bool {
Expand Down
40 changes: 40 additions & 0 deletions ethers-solc/tests/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,3 +613,43 @@ contract LinkTest {
let s = serde_json::to_string(&bytecode).unwrap();
assert_eq!(bytecode.clone(), serde_json::from_str(&s).unwrap());
}

#[test]
fn can_recompile_with_changes() {
let mut tmp = TempProject::dapptools().unwrap();
tmp.project_mut().allowed_lib_paths = vec![tmp.root().join("modules")].into();

let content = r#"
pragma solidity ^0.8.10;
import "../modules/B.sol";
contract A {}
"#;
tmp.add_source("A", content).unwrap();

tmp.add_contract(
"modules/B",
r#"
pragma solidity ^0.8.10;
contract B {}
"#,
)
.unwrap();

let compiled = tmp.compile().unwrap();
assert!(!compiled.has_compiler_errors());
assert!(compiled.find("A").is_some());
assert!(compiled.find("B").is_some());

let compiled = tmp.compile().unwrap();
assert!(compiled.find("A").is_some());
assert!(compiled.find("B").is_some());
assert!(compiled.is_unchanged());

// modify A.sol
tmp.add_source("A", format!("{}\n", content)).unwrap();
let compiled = tmp.compile().unwrap();
assert!(!compiled.has_compiler_errors());
assert!(!compiled.is_unchanged());
assert!(compiled.find("A").is_some());
assert!(compiled.find("B").is_some());
}