foundry_cli/opts/build/
utils.rs

1use eyre::Result;
2use foundry_compilers::{
3    CompilerInput, Graph, Project, ProjectCompileOutput, ProjectPathsConfig,
4    artifacts::{Source, Sources},
5    multi::{MultiCompilerLanguage, MultiCompilerParser},
6    solc::{SOLC_EXTENSIONS, SolcLanguage, SolcVersionedInput},
7};
8use foundry_config::{Config, semver::Version};
9use rayon::prelude::*;
10use solar::{interface::MIN_SOLIDITY_VERSION as MSV, sema::ParsingContext};
11use std::{
12    collections::{HashSet, VecDeque},
13    path::{Path, PathBuf},
14};
15
16const MIN_SUPPORTED_VERSION: Version = Version::new(MSV.0, MSV.1, MSV.2);
17
18/// Configures a [`ParsingContext`] from [`Config`].
19///
20/// - Configures include paths, remappings
21/// - Source files are added if `add_source_file` is set
22/// - If no `project` is provided, it will spin up a new ephemeral project.
23/// - If no `target_paths` are provided, all project files are processed.
24/// - Only processes the subset of sources with the most up-to-date Solidity version.
25pub fn configure_pcx(
26    pcx: &mut ParsingContext<'_>,
27    config: &Config,
28    project: Option<&Project>,
29    target_paths: Option<&[PathBuf]>,
30) -> Result<()> {
31    // Process build options
32    let project = match project {
33        Some(project) => project,
34        None => &config.ephemeral_project()?,
35    };
36
37    let sources = match target_paths {
38        // If target files are provided, only process those sources
39        Some(targets) => {
40            let mut sources = Sources::new();
41            for t in targets {
42                let path = dunce::canonicalize(t)?;
43                let source = Source::read(&path)?;
44                sources.insert(path, source);
45            }
46            sources
47        }
48        // Otherwise, process all project files
49        None => project.paths.read_input_files()?,
50    };
51
52    // Only process sources with latest Solidity version to avoid conflicts.
53    let graph = Graph::<MultiCompilerParser>::resolve_sources(&project.paths, sources)?;
54    let (version, sources) = graph
55        // Resolve graph into mapping language -> version -> sources
56        .into_sources_by_version(project)?
57        .sources
58        .into_iter()
59        // Only interested in Solidity sources
60        .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity))
61        .ok_or_else(|| eyre::eyre!("no Solidity sources"))?
62        .1
63        .into_iter()
64        // Filter unsupported versions
65        .filter(|(v, _, _)| v >= &MIN_SUPPORTED_VERSION)
66        // Always pick the latest version
67        .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2))
68        .map_or((MIN_SUPPORTED_VERSION, Sources::default()), |(v, s, _)| (v, s));
69
70    if sources.is_empty() {
71        sh_warn!("no files found. Solar doesn't support Solidity versions prior to 0.8.0")?;
72    }
73
74    let solc = SolcVersionedInput::build(
75        sources,
76        config.solc_settings()?,
77        SolcLanguage::Solidity,
78        version,
79    );
80
81    configure_pcx_from_solc(pcx, &project.paths, &solc, true);
82
83    Ok(())
84}
85
86/// Extracts Solar-compatible sources from a [`ProjectCompileOutput`].
87///
88/// # Note:
89/// uses `output.graph().source_files()` and `output.artifact_ids()` rather than `output.sources()`
90/// because sources aren't populated when build is skipped when there are no changes in the source
91/// code. <https://github.com/foundry-rs/foundry/issues/12018>
92pub fn get_solar_sources_from_compile_output(
93    config: &Config,
94    output: &ProjectCompileOutput,
95    target_paths: Option<&[PathBuf]>,
96) -> Result<SolcVersionedInput> {
97    let is_solidity_file = |path: &Path| -> bool {
98        path.extension().and_then(|s| s.to_str()).is_some_and(|ext| SOLC_EXTENSIONS.contains(&ext))
99    };
100
101    // Collect source path targets
102    let mut source_paths: HashSet<PathBuf> = if let Some(targets) = target_paths
103        && !targets.is_empty()
104    {
105        let mut source_paths = HashSet::new();
106        let mut queue: VecDeque<PathBuf> = targets
107            .iter()
108            .filter_map(|path| {
109                is_solidity_file(path).then(|| dunce::canonicalize(path).ok()).flatten()
110            })
111            .collect();
112
113        while let Some(path) = queue.pop_front() {
114            if source_paths.insert(path.clone()) {
115                for import in output.graph().imports(path.as_path()) {
116                    queue.push_back(import.to_path_buf());
117                }
118            }
119        }
120
121        source_paths
122    } else {
123        output
124            .graph()
125            .source_files()
126            .filter_map(|idx| {
127                let path = output.graph().node_path(idx).to_path_buf();
128                is_solidity_file(&path).then_some(path)
129            })
130            .collect()
131    };
132
133    // Read all sources and find the latest version.
134    let (version, sources) = {
135        let (mut max_version, mut sources) = (MIN_SUPPORTED_VERSION, Sources::new());
136        for (id, _) in output.artifact_ids() {
137            if let Ok(path) = dunce::canonicalize(&id.source)
138                && source_paths.remove(&path)
139            {
140                if id.version < MIN_SUPPORTED_VERSION {
141                    continue;
142                } else if max_version < id.version {
143                    max_version = id.version;
144                };
145
146                let source = Source::read(&path)?;
147                sources.insert(path, source);
148            }
149        }
150
151        (max_version, sources)
152    };
153
154    let solc = SolcVersionedInput::build(
155        sources,
156        config.solc_settings()?,
157        SolcLanguage::Solidity,
158        version,
159    );
160
161    Ok(solc)
162}
163
164/// Configures a [`ParsingContext`] from a [`ProjectCompileOutput`].
165pub fn configure_pcx_from_compile_output(
166    pcx: &mut ParsingContext<'_>,
167    config: &Config,
168    output: &ProjectCompileOutput,
169    target_paths: Option<&[PathBuf]>,
170) -> Result<()> {
171    let solc = get_solar_sources_from_compile_output(config, output, target_paths)?;
172    configure_pcx_from_solc(pcx, &config.project_paths(), &solc, true);
173    Ok(())
174}
175
176/// Configures a [`ParsingContext`] from [`ProjectPathsConfig`] and [`SolcVersionedInput`].
177///
178/// - Configures include paths, remappings.
179/// - Source files are added if `add_source_file` is set
180pub fn configure_pcx_from_solc(
181    pcx: &mut ParsingContext<'_>,
182    project_paths: &ProjectPathsConfig,
183    vinput: &SolcVersionedInput,
184    add_source_files: bool,
185) {
186    configure_pcx_from_solc_cli(pcx, project_paths, &vinput.cli_settings);
187    if add_source_files {
188        let sources = vinput
189            .input
190            .sources
191            .par_iter()
192            .filter_map(|(path, source)| {
193                pcx.sess.source_map().new_source_file(path.clone(), source.content.as_str()).ok()
194            })
195            .collect::<Vec<_>>();
196        pcx.add_files(sources);
197    }
198}
199
200fn configure_pcx_from_solc_cli(
201    pcx: &mut ParsingContext<'_>,
202    project_paths: &ProjectPathsConfig,
203    cli_settings: &foundry_compilers::solc::CliSettings,
204) {
205    pcx.file_resolver
206        .set_current_dir(cli_settings.base_path.as_ref().unwrap_or(&project_paths.root));
207    for remapping in &project_paths.remappings {
208        pcx.file_resolver.add_import_remapping(solar::sema::interface::config::ImportRemapping {
209            context: remapping.context.clone().unwrap_or_default(),
210            prefix: remapping.name.clone(),
211            path: remapping.path.clone(),
212        });
213    }
214    pcx.file_resolver.add_include_paths(cli_settings.include_paths.iter().cloned());
215}