Skip to content

Commit

Permalink
feat: allow multiple compiler configs (#170)
Browse files Browse the repository at this point in the history
This adds `CompilerSettings::Restrictions` associated type which allows
configuring constraints for compilation settings. Those constraints can
be checked against a settings object and merged.

Ideally given a set of constraints we'd be able to automatically resolve
configurations given only basic (default) config, though I think we can
do this in follow-up. Current implementation requires user to provide us
with concrete profiles such that for each defined constraint at least
one of the profiles matches it. This should be enough for most of the
usecases for now.

Profile to use is resolved in `Graph` similarly to version resolution.

The way this could look in foundry.toml is:
```toml

[profile.default]
# by default no via-ir and evm version is paris for everything
evm_version = "paris"
via_ir = false

# add cancun profile and via_ir profiles
additional_compiler_profiles = [{ evm_version = "cancun" }, {via_ir = true} ]

# enforce compiling some contracts with cancun, and some with via-ir
# if those contracts are imported by some items in the project, constraints will apply as well
compilation_constraints = [{path = "./**/*.cancun.sol", min_evm_version = "cancun" }, {path = "SomeComplexFile.sol", via_ir = true }] 
```

When writing artifacts, a profile name is added to artifact file name if
the same contract was compiled with multiple artifacts.
  • Loading branch information
klkvr authored Nov 15, 2024
1 parent 034ecd6 commit b5c2a49
Show file tree
Hide file tree
Showing 21 changed files with 842 additions and 309 deletions.
2 changes: 1 addition & 1 deletion crates/artifacts/solc/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ pub struct CompactContractRefSome<'a> {
pub bin_runtime: &'a BytecodeObject,
}

impl<'a> CompactContractRefSome<'a> {
impl CompactContractRefSome<'_> {
/// Returns the individual parts of this contract.
///
/// If the values are `None`, then `Default` is returned.
Expand Down
2 changes: 1 addition & 1 deletion crates/artifacts/solc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,7 @@ impl<'de> Deserialize<'de> for LosslessMetadata {
{
struct LosslessMetadataVisitor;

impl<'de> Visitor<'de> for LosslessMetadataVisitor {
impl Visitor<'_> for LosslessMetadataVisitor {
type Value = LosslessMetadata;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down
6 changes: 3 additions & 3 deletions crates/artifacts/solc/src/sourcemap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ enum Token<'a> {
Regular,
}

impl<'a> fmt::Debug for Token<'a> {
impl fmt::Debug for Token<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Number(s) => write!(f, "NUMBER({s:?})"),
Expand All @@ -105,7 +105,7 @@ impl<'a> fmt::Debug for Token<'a> {
}
}

impl<'a> fmt::Display for Token<'a> {
impl fmt::Display for Token<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Number(_) => write!(f, "number"),
Expand Down Expand Up @@ -531,7 +531,7 @@ impl<'input> Parser<'input> {
}
}

impl<'input> Iterator for Parser<'input> {
impl Iterator for Parser<'_> {
type Item = Result<SourceElement, SyntaxError>;

fn next(&mut self) -> Option<Self::Item> {
Expand Down
136 changes: 75 additions & 61 deletions crates/compilers/src/artifact_output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ mod hh;
pub use hh::*;

use crate::{
cache::{CachedArtifact, CompilerCache},
cache::{CachedArtifacts, CompilerCache},
output::{
contracts::VersionedContracts,
sources::{VersionedSourceFile, VersionedSourceFiles},
Expand All @@ -52,6 +52,7 @@ pub struct ArtifactId {
pub version: Version,
/// `solc` build id
pub build_id: String,
pub profile: String,
}

impl ArtifactId {
Expand Down Expand Up @@ -119,6 +120,7 @@ pub struct ArtifactFile<T> {
/// `solc` version that produced this artifact
pub version: Version,
pub build_id: String,
pub profile: String,
}

impl<T: Serialize> ArtifactFile<T> {
Expand Down Expand Up @@ -298,6 +300,7 @@ impl<T> Artifacts<T> {
source: source.clone(),
version: artifact.version.clone(),
build_id: artifact.build_id.clone(),
profile: artifact.profile.clone(),
}
.with_slashed_paths(),
&artifact.artifact,
Expand All @@ -324,6 +327,7 @@ impl<T> Artifacts<T> {
source: source.clone(),
version: artifact.version,
build_id: artifact.build_id.clone(),
profile: artifact.profile.clone(),
}
.with_slashed_paths(),
artifact.artifact,
Expand Down Expand Up @@ -642,14 +646,22 @@ pub trait ArtifactOutput {

/// Returns the file name for the contract's artifact
/// `Greeter.json`
fn output_file_name(name: &str) -> PathBuf {
format!("{name}.json").into()
}

/// Returns the file name for the contract's artifact and the given version
/// `Greeter.0.8.11.json`
fn output_file_name_versioned(name: &str, version: &Version) -> PathBuf {
format!("{}.{}.{}.{}.json", name, version.major, version.minor, version.patch).into()
fn output_file_name(
name: &str,
version: &Version,
profile: &str,
with_version: bool,
with_profile: bool,
) -> PathBuf {
let mut name = name.to_string();
if with_version {
name.push_str(&format!(".{}.{}.{}", version.major, version.minor, version.patch));
}
if with_profile {
name.push_str(&format!(".{profile}"));
}
name.push_str(".json");
name.into()
}

/// Returns the appropriate file name for the conflicting file.
Expand Down Expand Up @@ -724,24 +736,23 @@ pub trait ArtifactOutput {
/// Returns the path to the contract's artifact location based on the contract's file and name
///
/// This returns `contract.sol/contract.json` by default
fn output_file(contract_file: &Path, name: &str) -> PathBuf {
contract_file
.file_name()
.map(Path::new)
.map(|p| p.join(Self::output_file_name(name)))
.unwrap_or_else(|| Self::output_file_name(name))
}

/// Returns the path to the contract's artifact location based on the contract's file, name and
/// version
///
/// This returns `contract.sol/contract.0.8.11.json` by default
fn output_file_versioned(contract_file: &Path, name: &str, version: &Version) -> PathBuf {
fn output_file(
contract_file: &Path,
name: &str,
version: &Version,
profile: &str,
with_version: bool,
with_profile: bool,
) -> PathBuf {
contract_file
.file_name()
.map(Path::new)
.map(|p| p.join(Self::output_file_name_versioned(name, version)))
.unwrap_or_else(|| Self::output_file_name_versioned(name, version))
.map(|p| {
p.join(Self::output_file_name(name, version, profile, with_version, with_profile))
})
.unwrap_or_else(|| {
Self::output_file_name(name, version, profile, with_version, with_profile)
})
}

/// The inverse of `contract_file_name`
Expand All @@ -752,11 +763,6 @@ pub trait ArtifactOutput {
file.file_stem().and_then(|s| s.to_str().map(|s| s.to_string()))
}

/// Whether the corresponding artifact of the given contract file and name exists
fn output_exists(contract_file: &Path, name: &str, root: &Path) -> bool {
root.join(Self::output_file(contract_file, name)).exists()
}

/// Read the artifact that's stored at the given path
///
/// # Errors
Expand Down Expand Up @@ -800,28 +806,27 @@ pub trait ArtifactOutput {

/// Generates a path for an artifact based on already taken paths by either cached or compiled
/// artifacts.
#[allow(clippy::too_many_arguments)]
fn get_artifact_path(
ctx: &OutputContext<'_>,
already_taken: &HashSet<String>,
file: &Path,
name: &str,
artifacts_folder: &Path,
version: &Version,
versioned: bool,
profile: &str,
with_version: bool,
with_profile: bool,
) -> PathBuf {
// if an artifact for the contract already exists (from a previous compile job)
// we reuse the path, this will make sure that even if there are conflicting
// files (files for witch `T::output_file()` would return the same path) we use
// consistent output paths
if let Some(existing_artifact) = ctx.existing_artifact(file, name, version) {
if let Some(existing_artifact) = ctx.existing_artifact(file, name, version, profile) {
trace!("use existing artifact file {:?}", existing_artifact,);
existing_artifact.to_path_buf()
} else {
let path = if versioned {
Self::output_file_versioned(file, name, version)
} else {
Self::output_file(file, name)
};
let path = Self::output_file(file, name, version, profile, with_version, with_profile);

let path = artifacts_folder.join(path);

Expand Down Expand Up @@ -854,7 +859,9 @@ pub trait ArtifactOutput {
let mut taken_paths_lowercase = ctx
.existing_artifacts
.values()
.flat_map(|artifacts| artifacts.values().flat_map(|artifacts| artifacts.values()))
.flat_map(|artifacts| artifacts.values())
.flat_map(|artifacts| artifacts.values())
.flat_map(|artifacts| artifacts.values())
.map(|a| a.path.to_slash_lossy().to_lowercase())
.collect::<HashSet<_>>();

Expand All @@ -865,22 +872,26 @@ pub trait ArtifactOutput {
});
for file in files {
for (name, versioned_contracts) in &contracts[file] {
let unique_versions =
versioned_contracts.iter().map(|c| &c.version).collect::<HashSet<_>>();
let unique_profiles =
versioned_contracts.iter().map(|c| &c.profile).collect::<HashSet<_>>();
for contract in versioned_contracts {
non_standalone_sources.insert(file);

// track `SourceFile`s that can be mapped to contracts
let source_file = sources.find_file_and_version(file, &contract.version);

if let Some(source) = source_file {
non_standalone_sources.insert((source.id, &contract.version));
}

let artifact_path = Self::get_artifact_path(
&ctx,
&taken_paths_lowercase,
file,
name,
layout.artifacts.as_path(),
&contract.version,
versioned_contracts.len() > 1,
&contract.profile,
unique_versions.len() > 1,
unique_profiles.len() > 1,
);

taken_paths_lowercase.insert(artifact_path.to_slash_lossy().to_lowercase());
Expand All @@ -904,6 +915,7 @@ pub trait ArtifactOutput {
file: artifact_path,
version: contract.version.clone(),
build_id: contract.build_id.clone(),
profile: contract.profile.clone(),
};

artifacts
Expand All @@ -921,8 +933,10 @@ pub trait ArtifactOutput {
// any contract definition, which are not included in the `CompilerOutput` but we want to
// create Artifacts for them regardless
for (file, sources) in sources.as_ref().iter() {
let unique_versions = sources.iter().map(|s| &s.version).collect::<HashSet<_>>();
let unique_profiles = sources.iter().map(|s| &s.profile).collect::<HashSet<_>>();
for source in sources {
if !non_standalone_sources.contains(&(source.source_file.id, &source.version)) {
if !non_standalone_sources.contains(file) {
// scan the ast as a safe measure to ensure this file does not include any
// source units
// there's also no need to create a standalone artifact for source files that
Expand All @@ -945,26 +959,26 @@ pub trait ArtifactOutput {
name,
&layout.artifacts,
&source.version,
sources.len() > 1,
&source.profile,
unique_versions.len() > 1,
unique_profiles.len() > 1,
);

let entries = artifacts
taken_paths_lowercase
.insert(artifact_path.to_slash_lossy().to_lowercase());

artifacts
.entry(file.clone())
.or_default()
.entry(name.to_string())
.or_default();

if entries.iter().all(|entry| entry.version != source.version) {
taken_paths_lowercase
.insert(artifact_path.to_slash_lossy().to_lowercase());

entries.push(ArtifactFile {
.or_default()
.push(ArtifactFile {
artifact,
file: artifact_path,
version: source.version.clone(),
build_id: source.build_id.clone(),
profile: source.profile.clone(),
});
}
}
}
}
Expand Down Expand Up @@ -1015,8 +1029,7 @@ pub struct OutputContext<'a> {
/// └── inner
/// └── a.sol
/// ```
pub existing_artifacts:
BTreeMap<&'a Path, &'a BTreeMap<String, BTreeMap<Version, CachedArtifact>>>,
pub existing_artifacts: BTreeMap<&'a Path, &'a CachedArtifacts>,
}

// === impl OutputContext
Expand All @@ -1042,13 +1055,14 @@ impl<'a> OutputContext<'a> {
file: &Path,
contract: &str,
version: &Version,
profile: &str,
) -> Option<&Path> {
self.existing_artifacts.get(file).and_then(|contracts| {
contracts
.get(contract)
.and_then(|versions| versions.get(version))
.map(|a| a.path.as_path())
})
self.existing_artifacts
.get(file)
.and_then(|contracts| contracts.get(contract))
.and_then(|versions| versions.get(version))
.and_then(|profiles| profiles.get(profile))
.map(|a| a.path.as_path())
}
}

Expand Down
Loading

0 comments on commit b5c2a49

Please sign in to comment.