Skip to main content

foundry_cli/opts/build/
utils.rs

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