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