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