Skip to main content

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;
9use rayon::prelude::*;
10use solar::{interface::MIN_SOLIDITY_VERSION, sema::ParsingContext};
11use std::{
12    collections::{HashSet, VecDeque},
13    path::{Path, PathBuf},
14};
15
16/// Configures a [`ParsingContext`] from [`Config`].
17///
18/// - Configures include paths, remappings
19/// - Source files are added if `add_source_file` is set
20/// - If no `project` is provided, it will spin up a new ephemeral project.
21/// - If no `target_paths` are provided, all project files are processed.
22/// - Only processes the subset of sources with the most up-to-date Solidity version.
23pub fn configure_pcx(
24    pcx: &mut ParsingContext<'_>,
25    config: &Config,
26    project: Option<&Project>,
27    target_paths: Option<&[PathBuf]>,
28) -> Result<()> {
29    let status = configure_pcx_with_sources(pcx, config, project, target_paths, false)?;
30    if !status.has_compatible_sources && !status.has_unsupported_sources {
31        eyre::bail!("no Solidity sources");
32    }
33    Ok(())
34}
35
36/// Describes the Solidity sources encountered while configuring Solar.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct SolarSourceStatus {
39    /// Whether any Solar-compatible sources were configured.
40    has_compatible_sources: bool,
41    /// Whether any sources require a Solidity version unsupported by Solar.
42    has_unsupported_sources: bool,
43}
44
45impl SolarSourceStatus {
46    /// Returns whether compatible sources were configured and no unsupported sources were found.
47    pub const fn is_fully_supported(&self) -> bool {
48        self.has_compatible_sources && !self.has_unsupported_sources
49    }
50}
51
52/// Configures a Solar parsing context with all Solar-compatible project sources.
53///
54/// Returns `true` if any compatible sources were configured.
55pub fn configure_pcx_all_sources(
56    pcx: &mut ParsingContext<'_>,
57    config: &Config,
58    project: Option<&Project>,
59    target_paths: Option<&[PathBuf]>,
60) -> Result<bool> {
61    let status = configure_pcx_all_sources_with_status(pcx, config, project, target_paths)?;
62    if !status.has_compatible_sources && !status.has_unsupported_sources {
63        eyre::bail!("no Solidity sources");
64    }
65    Ok(status.has_compatible_sources)
66}
67
68/// Configures a Solar parsing context with all Solar-compatible project sources and reports
69/// whether unsupported sources were skipped.
70pub fn configure_pcx_all_sources_with_status(
71    pcx: &mut ParsingContext<'_>,
72    config: &Config,
73    project: Option<&Project>,
74    target_paths: Option<&[PathBuf]>,
75) -> Result<SolarSourceStatus> {
76    configure_pcx_with_sources(pcx, config, project, target_paths, true)
77}
78
79fn configure_pcx_with_sources(
80    pcx: &mut ParsingContext<'_>,
81    config: &Config,
82    project: Option<&Project>,
83    target_paths: Option<&[PathBuf]>,
84    all_versions: bool,
85) -> Result<SolarSourceStatus> {
86    // Process build options
87    let project = match project {
88        Some(project) => project,
89        None => &config.ephemeral_project()?,
90    };
91
92    let sources = match target_paths {
93        // If target files are provided, only process those sources
94        Some(targets) => {
95            let mut sources = Sources::new();
96            for t in targets {
97                let path = dunce::canonicalize(t)?;
98                let source = Source::read(&path)?;
99                sources.insert(path, source);
100            }
101            sources
102        }
103        // Otherwise, process all project files
104        None => {
105            let mut sources = project.paths.read_input_files()?;
106            if let Some(filter) = &project.sparse_output {
107                sources.retain(|path, _| filter.is_match(path));
108            }
109            sources
110        }
111    };
112
113    // Process Solar-compatible sources and use the latest version for the compiler input.
114    let graph = Graph::<MultiCompilerParser>::resolve_sources(&project.paths, sources)?;
115    let Some(versioned_sources) = graph
116        // Resolve graph into mapping language -> version -> sources
117        .into_sources_by_version(project)?
118        .sources
119        .into_iter()
120        // Only interested in Solidity sources
121        .find(|(lang, _)| *lang == MultiCompilerLanguage::Solc(SolcLanguage::Solidity))
122        .map(|(_, sources)| sources)
123    else {
124        return Ok(SolarSourceStatus::default());
125    };
126
127    let mut has_unsupported_sources = false;
128    let versioned_sources = versioned_sources.into_iter().filter(|(version, sources, _)| {
129        if version < &MIN_SOLIDITY_VERSION && !sources.is_empty() {
130            has_unsupported_sources = true;
131            false
132        } else {
133            true
134        }
135    });
136    let (version, sources) = if all_versions {
137        versioned_sources.fold(
138            (MIN_SOLIDITY_VERSION, Sources::default()),
139            |(version, mut sources), (source_version, version_sources, _)| {
140                sources.extend(version_sources);
141                (version.max(source_version), sources)
142            },
143        )
144    } else {
145        versioned_sources
146            .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2))
147            .map_or((MIN_SOLIDITY_VERSION, Sources::default()), |(v, s, _)| (v, s))
148    };
149
150    let has_sources = !sources.is_empty();
151    if !all_versions && !has_sources {
152        sh_warn!("no files found. Solar doesn't support Solidity versions prior to 0.8.0")?;
153    }
154
155    let solc = SolcVersionedInput::build(
156        sources,
157        config.solc_settings()?,
158        SolcLanguage::Solidity,
159        version,
160    );
161
162    configure_pcx_from_solc(pcx, &project.paths, &solc, true);
163
164    Ok(SolarSourceStatus { has_compatible_sources: has_sources, has_unsupported_sources })
165}
166
167/// Extracts Solar-compatible sources from a [`ProjectCompileOutput`].
168///
169/// # Note:
170/// uses `output.graph().source_files()` and `output.artifact_ids()` rather than `output.sources()`
171/// because sources aren't populated when build is skipped when there are no changes in the source
172/// code. <https://github.com/foundry-rs/foundry/issues/12018>
173pub fn get_solar_sources_from_compile_output(
174    config: &Config,
175    output: &ProjectCompileOutput,
176    target_paths: Option<&[PathBuf]>,
177    ignored_paths: Option<&[PathBuf]>,
178) -> Result<SolcVersionedInput> {
179    let is_solidity_file = |path: &Path| -> bool {
180        path.extension().and_then(|s| s.to_str()).is_some_and(|ext| SOLC_EXTENSIONS.contains(&ext))
181    };
182
183    let is_ignored = |path: &Path| -> bool {
184        if let Some(ignored) = ignored_paths {
185            ignored.iter().any(|ignored_path| path == ignored_path)
186        } else {
187            false
188        }
189    };
190
191    // Collect source path targets
192    let mut source_paths: HashSet<PathBuf> = if let Some(targets) = target_paths
193        && !targets.is_empty()
194    {
195        let mut source_paths = HashSet::new();
196        let mut queue: VecDeque<PathBuf> = targets
197            .iter()
198            .filter_map(|path| {
199                is_solidity_file(path).then(|| dunce::canonicalize(path).ok()).flatten()
200            })
201            .collect();
202
203        while let Some(path) = queue.pop_front() {
204            if source_paths.insert(path.clone()) {
205                for import in output.graph().imports(path.as_path()) {
206                    // Skip ignored imports to prevent solar from trying to compile them
207                    if !is_ignored(import) {
208                        queue.push_back(import.to_path_buf());
209                    }
210                }
211            }
212        }
213
214        source_paths
215    } else {
216        output
217            .graph()
218            .source_files()
219            .filter_map(|idx| {
220                let path = output.graph().node_path(idx).to_path_buf();
221                is_solidity_file(&path).then_some(path)
222            })
223            .collect()
224    };
225
226    // Read all sources and find the latest version.
227    let (version, sources) = {
228        let (mut max_version, mut sources) = (MIN_SOLIDITY_VERSION, Sources::new());
229        for (id, _) in output.artifact_ids() {
230            if let Ok(path) = dunce::canonicalize(&id.source)
231                && source_paths.remove(&path)
232            {
233                if id.version < MIN_SOLIDITY_VERSION {
234                    continue;
235                } else if max_version < id.version {
236                    max_version = id.version;
237                };
238
239                let source = Source::read(&path)?;
240                sources.insert(path, source);
241            }
242        }
243
244        (max_version, sources)
245    };
246
247    let solc = SolcVersionedInput::build(
248        sources,
249        config.solc_settings()?,
250        SolcLanguage::Solidity,
251        version,
252    );
253
254    Ok(solc)
255}
256
257/// Configures a [`ParsingContext`] from a [`ProjectCompileOutput`].
258pub fn configure_pcx_from_compile_output(
259    pcx: &mut ParsingContext<'_>,
260    config: &Config,
261    output: &ProjectCompileOutput,
262    target_paths: Option<&[PathBuf]>,
263) -> Result<()> {
264    let solc = get_solar_sources_from_compile_output(config, output, target_paths, None)?;
265    configure_pcx_from_solc(pcx, &config.project_paths(), &solc, true);
266    Ok(())
267}
268
269/// Configures a [`ParsingContext`] from [`ProjectPathsConfig`] and [`SolcVersionedInput`].
270///
271/// - Configures include paths, remappings.
272/// - Source files are added if `add_source_file` is set
273pub fn configure_pcx_from_solc(
274    pcx: &mut ParsingContext<'_>,
275    project_paths: &ProjectPathsConfig,
276    vinput: &SolcVersionedInput,
277    add_source_files: bool,
278) {
279    configure_pcx_from_solc_cli(pcx, project_paths, &vinput.cli_settings);
280    if add_source_files {
281        let sources = vinput
282            .input
283            .sources
284            .par_iter()
285            .filter_map(|(path, source)| {
286                pcx.sess.source_map().new_source_file(path.clone(), source.content.as_str()).ok()
287            })
288            .collect::<Vec<_>>();
289        pcx.add_files(sources);
290    }
291}
292
293fn configure_pcx_from_solc_cli(
294    pcx: &mut ParsingContext<'_>,
295    project_paths: &ProjectPathsConfig,
296    cli_settings: &foundry_compilers::solc::CliSettings,
297) {
298    pcx.file_resolver
299        .set_current_dir(cli_settings.base_path.as_ref().unwrap_or(&project_paths.root));
300    for remapping in &project_paths.remappings {
301        pcx.file_resolver.add_import_remapping(solar::sema::interface::config::ImportRemapping {
302            context: remapping.context.clone().unwrap_or_default(),
303            prefix: remapping.name.clone(),
304            path: remapping.path.clone(),
305        });
306    }
307    pcx.file_resolver.add_include_paths(cli_settings.include_paths.iter().cloned());
308}