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