foundry_compilers/compilers/solc/
compiler.rs1use crate::resolver::parse::SolData;
2use foundry_compilers_artifacts::{sources::Source, CompilerOutput, SolcInput};
3use foundry_compilers_core::{
4 error::{Result, SolcError},
5 utils::{self, SUPPORTS_BASE_PATH, SUPPORTS_INCLUDE_PATH},
6};
7use itertools::Itertools;
8use semver::{Version, VersionReq};
9use serde::{de::DeserializeOwned, Deserialize, Serialize};
10use std::{
11 collections::BTreeSet,
12 io::{self, Write},
13 path::{Path, PathBuf},
14 process::{Command, Output, Stdio},
15 str::FromStr,
16};
17
18pub const SOLC_EXTENSIONS: &[&str] = &["sol", "yul"];
20
21#[cfg(feature = "svm-solc")]
28#[cfg(any(test, feature = "test-utils"))]
29#[macro_export]
30macro_rules! take_solc_installer_lock {
31 ($lock:ident) => {
32 let lock_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(".lock");
33 let lock_file = std::fs::OpenOptions::new()
34 .read(true)
35 .write(true)
36 .create(true)
37 .truncate(false)
38 .open(lock_path)
39 .unwrap();
40 let mut lock = fd_lock::RwLock::new(lock_file);
41 let $lock = lock.write().unwrap();
42 };
43}
44
45#[cfg(feature = "svm-solc")]
49pub static RELEASES: std::sync::LazyLock<(svm::Releases, Vec<Version>, bool)> =
50 std::sync::LazyLock::new(|| {
51 match serde_json::from_str::<svm::Releases>(svm_builds::RELEASE_LIST_JSON) {
52 Ok(releases) => {
53 let sorted_versions = releases.clone().into_versions();
54 (releases, sorted_versions, true)
55 }
56 Err(err) => {
57 error!("failed to deserialize SVM static RELEASES JSON: {err}");
58 Default::default()
59 }
60 }
61 });
62
63#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
73pub struct Solc {
74 pub solc: PathBuf,
76 pub version: Version,
78 pub base_path: Option<PathBuf>,
80 pub allow_paths: BTreeSet<PathBuf>,
82 pub include_paths: BTreeSet<PathBuf>,
84 pub extra_args: Vec<String>,
86}
87
88impl Solc {
89 pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
93 let path = path.into();
94 let version = Self::version(path.clone())?;
95 Ok(Self::new_with_version(path, version))
96 }
97
98 pub fn new_with_args(
103 path: impl Into<PathBuf>,
104 extra_args: impl IntoIterator<Item: Into<String>>,
105 ) -> Result<Self> {
106 let args = extra_args.into_iter().map(Into::into).collect::<Vec<_>>();
107 let path = path.into();
108 let version = Self::version_with_args(path.clone(), &args)?;
109
110 let mut solc = Self::new_with_version(path, version);
111 solc.extra_args = args;
112
113 Ok(solc)
114 }
115
116 pub fn new_with_version(path: impl Into<PathBuf>, version: Version) -> Self {
118 Self {
119 solc: path.into(),
120 version,
121 base_path: None,
122 allow_paths: Default::default(),
123 include_paths: Default::default(),
124 extra_args: Default::default(),
125 }
126 }
127
128 pub fn source_version_req(source: &Source) -> Result<VersionReq> {
131 Ok(SolData::parse_version_pragma(&source.content).ok_or(SolcError::PragmaNotFound)??)
132 }
133
134 #[cfg(feature = "svm-solc")]
139 pub fn detect_version(source: &Source) -> Result<Version> {
140 let sol_version = Self::source_version_req(source)?;
142 Self::ensure_installed(&sol_version)
143 }
144
145 #[cfg(feature = "svm-solc")]
150 pub fn ensure_installed(sol_version: &VersionReq) -> Result<Version> {
151 #[cfg(test)]
152 take_solc_installer_lock!(_lock);
153
154 let versions = Self::installed_versions();
156
157 let local_versions = Self::find_matching_installation(&versions, sol_version);
158 let remote_versions = Self::find_matching_installation(&RELEASES.1, sol_version);
159
160 Ok(match (local_versions, remote_versions) {
162 (Some(local), None) => local,
163 (Some(local), Some(remote)) => {
164 if remote > local {
165 Self::blocking_install(&remote)?;
166 remote
167 } else {
168 local
169 }
170 }
171 (None, Some(version)) => {
172 Self::blocking_install(&version)?;
173 version
174 }
175 _ => return Err(SolcError::VersionNotFound),
177 })
178 }
179
180 pub fn find_matching_installation(
183 versions: &[Version],
184 required_version: &VersionReq,
185 ) -> Option<Version> {
186 versions.iter().rev().find(|version| required_version.matches(version)).cloned()
188 }
189
190 pub fn find_svm_installed_version(version: &Version) -> Result<Option<Self>> {
204 let version = format!("{}.{}.{}", version.major, version.minor, version.patch);
205 let solc = Self::svm_home()
206 .ok_or_else(|| SolcError::msg("svm home dir not found"))?
207 .join(&version)
208 .join(format!("solc-{version}"));
209
210 if !solc.is_file() {
211 return Ok(None);
212 }
213 Self::new(&solc).map(Some)
214 }
215
216 pub fn svm_home() -> Option<PathBuf> {
222 if let Some(home_dir) = home::home_dir() {
223 let home_dot_svm = home_dir.join(".svm");
224 if home_dot_svm.exists() {
225 return Some(home_dot_svm);
226 }
227 }
228 dirs::data_dir().map(|dir| dir.join("svm"))
229 }
230
231 pub fn svm_global_version() -> Option<Version> {
237 let home = Self::svm_home()?;
238 let version = std::fs::read_to_string(home.join(".global_version")).ok()?;
239 Version::parse(&version).ok()
240 }
241
242 pub fn installed_versions() -> Vec<Version> {
244 Self::svm_home()
245 .map(|home| utils::installed_versions(&home).unwrap_or_default())
246 .unwrap_or_default()
247 }
248
249 #[cfg(feature = "svm-solc")]
251 pub fn released_versions() -> Vec<Version> {
252 RELEASES.1.clone().into_iter().collect()
253 }
254
255 #[cfg(feature = "svm-solc")]
269 pub async fn install(version: &Version) -> std::result::Result<Self, svm::SvmError> {
270 trace!("installing solc version \"{}\"", version);
271 crate::report::solc_installation_start(version);
272 match svm::install(version).await {
273 Ok(path) => {
274 crate::report::solc_installation_success(version);
275 Ok(Self::new_with_version(path, version.clone()))
276 }
277 Err(err) => {
278 crate::report::solc_installation_error(version, &err.to_string());
279 Err(err)
280 }
281 }
282 }
283
284 #[cfg(feature = "svm-solc")]
286 pub fn blocking_install(version: &Version) -> std::result::Result<Self, svm::SvmError> {
287 use foundry_compilers_core::utils::RuntimeOrHandle;
288
289 #[cfg(test)]
290 crate::take_solc_installer_lock!(_lock);
291
292 let version = Version::new(version.major, version.minor, version.patch);
293
294 trace!("blocking installing solc version \"{}\"", version);
295 crate::report::solc_installation_start(&version);
296 match RuntimeOrHandle::new().block_on(svm::install(&version)) {
300 Ok(path) => {
301 crate::report::solc_installation_success(&version);
302 Ok(Self::new_with_version(path, version.clone()))
303 }
304 Err(err) => {
305 crate::report::solc_installation_error(&version, &err.to_string());
306 Err(err)
307 }
308 }
309 }
310
311 #[cfg(feature = "svm-solc")]
314 pub fn verify_checksum(&self) -> Result<()> {
315 let version = self.version_short();
316 let mut version_path = svm::version_path(version.to_string().as_str());
317 version_path.push(format!("solc-{}", version.to_string().as_str()));
318 trace!(target:"solc", "reading solc binary for checksum {:?}", version_path);
319 let content =
320 std::fs::read(&version_path).map_err(|err| SolcError::io(err, version_path.clone()))?;
321
322 if !RELEASES.2 {
323 return Ok(());
326 }
327
328 #[cfg(windows)]
329 {
330 const V0_7_2: Version = Version::new(0, 7, 2);
333 if version < V0_7_2 {
334 return Ok(());
335 }
336 }
337
338 use sha2::Digest;
339 let mut hasher = sha2::Sha256::new();
340 hasher.update(content);
341 let checksum_calc = &hasher.finalize()[..];
342
343 let checksum_found = &RELEASES
344 .0
345 .get_checksum(&version)
346 .ok_or_else(|| SolcError::ChecksumNotFound { version: version.clone() })?;
347
348 if checksum_calc == checksum_found {
349 Ok(())
350 } else {
351 use alloy_primitives::hex;
352 let expected = hex::encode(checksum_found);
353 let detected = hex::encode(checksum_calc);
354 warn!(target: "solc", "checksum mismatch for {:?}, expected {}, but found {} for file {:?}", version, expected, detected, version_path);
355 Err(SolcError::ChecksumMismatch { version, expected, detected, file: version_path })
356 }
357 }
358
359 pub fn compile_source(&self, path: &Path) -> Result<CompilerOutput> {
361 let mut res: CompilerOutput = Default::default();
362 for input in
363 SolcInput::resolve_and_build(Source::read_sol_yul_from(path)?, Default::default())
364 {
365 let input = input.sanitized(&self.version);
366 let output = self.compile(&input)?;
367 res.merge(output)
368 }
369
370 Ok(res)
371 }
372
373 pub fn compile_exact(&self, input: &SolcInput) -> Result<CompilerOutput> {
381 let mut out = self.compile(input)?;
382 out.retain_files(input.sources.keys().map(|p| p.as_path()));
383 Ok(out)
384 }
385
386 pub fn compile<T: Serialize>(&self, input: &T) -> Result<CompilerOutput> {
406 self.compile_as(input)
407 }
408
409 pub fn compile_as<T: Serialize, D: DeserializeOwned>(&self, input: &T) -> Result<D> {
411 let output = self.compile_output(input)?;
412
413 let output = std::str::from_utf8(&output).map_err(|_| SolcError::InvalidUtf8)?;
415
416 Ok(serde_json::from_str(output)?)
417 }
418
419 #[instrument(name = "compile", level = "debug", skip_all)]
421 pub fn compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
422 let mut cmd = self.configure_cmd();
423
424 trace!(input=%serde_json::to_string(input).unwrap_or_else(|e| e.to_string()));
425 debug!(?cmd, "compiling");
426
427 let mut child = cmd.spawn().map_err(self.map_io_err())?;
428 debug!("spawned");
429
430 {
431 let mut stdin = io::BufWriter::new(child.stdin.take().unwrap());
432 serde_json::to_writer(&mut stdin, input)?;
433 stdin.flush().map_err(self.map_io_err())?;
434 }
435 debug!("wrote JSON input to stdin");
436
437 let output = child.wait_with_output().map_err(self.map_io_err())?;
438 debug!(%output.status, output.stderr = ?String::from_utf8_lossy(&output.stderr), "finished");
439
440 compile_output(output)
441 }
442
443 pub fn version_short(&self) -> Version {
446 Version::new(self.version.major, self.version.minor, self.version.patch)
447 }
448
449 #[instrument(level = "debug", skip_all)]
451 pub fn version(solc: impl Into<PathBuf>) -> Result<Version> {
452 Self::version_with_args(solc, &[])
453 }
454
455 #[instrument(level = "debug", skip_all)]
457 pub fn version_with_args(solc: impl Into<PathBuf>, args: &[String]) -> Result<Version> {
458 crate::cache_version(solc.into(), args, |solc| {
459 let mut cmd = Command::new(solc);
460 cmd.args(args)
461 .arg("--version")
462 .stdin(Stdio::piped())
463 .stderr(Stdio::piped())
464 .stdout(Stdio::piped());
465 debug!(?cmd, "getting Solc version");
466 let output = cmd.output().map_err(|e| SolcError::io(e, solc))?;
467 trace!(?output);
468 let version = version_from_output(output)?;
469 debug!(%version);
470 Ok(version)
471 })
472 }
473
474 fn map_io_err(&self) -> impl FnOnce(std::io::Error) -> SolcError + '_ {
475 move |err| SolcError::io(err, &self.solc)
476 }
477
478 pub fn configure_cmd(&self) -> Command {
482 let mut cmd = Command::new(&self.solc);
483 cmd.stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped());
484 cmd.args(&self.extra_args);
485
486 if !self.allow_paths.is_empty() {
487 cmd.arg("--allow-paths");
488 cmd.arg(self.allow_paths.iter().map(|p| p.display()).join(","));
489 }
490 if let Some(base_path) = &self.base_path {
491 if SUPPORTS_BASE_PATH.matches(&self.version) {
492 if SUPPORTS_INCLUDE_PATH.matches(&self.version) {
493 for path in
497 self.include_paths.iter().filter(|p| p.as_path() != base_path.as_path())
498 {
499 cmd.arg("--include-path").arg(path);
500 }
501 }
502
503 cmd.arg("--base-path").arg(base_path);
504 }
505
506 cmd.current_dir(base_path);
507 }
508
509 cmd.arg("--standard-json");
510
511 cmd
512 }
513
514 #[cfg(feature = "svm-solc")]
516 pub fn find_or_install(version: &Version) -> Result<Self> {
517 let solc = if let Some(solc) = Self::find_svm_installed_version(version)? {
518 solc
519 } else {
520 Self::blocking_install(version)?
521 };
522
523 Ok(solc)
524 }
525}
526
527#[cfg(feature = "async")]
528impl Solc {
529 pub async fn async_compile_source(&self, path: &Path) -> Result<CompilerOutput> {
531 self.async_compile(&SolcInput::resolve_and_build(
532 Source::async_read_all_from(path, SOLC_EXTENSIONS).await?,
533 Default::default(),
534 ))
535 .await
536 }
537
538 pub async fn async_compile<T: Serialize>(&self, input: &T) -> Result<CompilerOutput> {
541 self.async_compile_as(input).await
542 }
543
544 pub async fn async_compile_as<T: Serialize, D: DeserializeOwned>(
547 &self,
548 input: &T,
549 ) -> Result<D> {
550 let output = self.async_compile_output(input).await?;
551 Ok(serde_json::from_slice(&output)?)
552 }
553
554 pub async fn async_compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
555 use tokio::{io::AsyncWriteExt, process::Command};
556
557 let mut cmd: Command = self.configure_cmd().into();
558 let mut child = cmd.spawn().map_err(self.map_io_err())?;
559 let stdin = child.stdin.as_mut().unwrap();
560
561 let content = serde_json::to_vec(input)?;
562
563 stdin.write_all(&content).await.map_err(self.map_io_err())?;
564 stdin.flush().await.map_err(self.map_io_err())?;
565
566 compile_output(child.wait_with_output().await.map_err(self.map_io_err())?)
567 }
568
569 pub async fn async_version(solc: &Path) -> Result<Version> {
570 let mut cmd = tokio::process::Command::new(solc);
571 cmd.arg("--version").stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped());
572 debug!(?cmd, "getting version");
573 let output = cmd.output().await.map_err(|e| SolcError::io(e, solc))?;
574 let version = version_from_output(output)?;
575 debug!(%version);
576 Ok(version)
577 }
578
579 pub async fn compile_many<I>(jobs: I, n: usize) -> crate::many::CompiledMany
585 where
586 I: IntoIterator<Item = (Self, SolcInput)>,
587 {
588 use futures_util::stream::StreamExt;
589
590 let outputs = futures_util::stream::iter(
591 jobs.into_iter()
592 .map(|(solc, input)| async { (solc.async_compile(&input).await, solc, input) }),
593 )
594 .buffer_unordered(n)
595 .collect::<Vec<_>>()
596 .await;
597
598 crate::many::CompiledMany::new(outputs)
599 }
600}
601
602fn compile_output(output: Output) -> Result<Vec<u8>> {
603 if output.status.success() {
604 Ok(output.stdout)
605 } else {
606 Err(SolcError::solc_output(&output))
607 }
608}
609
610fn version_from_output(output: Output) -> Result<Version> {
611 if output.status.success() {
612 let stdout = String::from_utf8_lossy(&output.stdout);
613 let version = stdout
614 .lines()
615 .filter(|l| !l.trim().is_empty())
616 .next_back()
617 .ok_or_else(|| SolcError::msg("Version not found in Solc output"))?;
618 Ok(Version::from_str(&version.trim_start_matches("Version: ").replace(".g++", ".gcc"))?)
620 } else {
621 Err(SolcError::solc_output(&output))
622 }
623}
624
625impl AsRef<Path> for Solc {
626 fn as_ref(&self) -> &Path {
627 &self.solc
628 }
629}
630
631#[cfg(test)]
632#[cfg(feature = "svm-solc")]
633mod tests {
634 use super::*;
635 use crate::{resolver::parse::SolData, Artifact};
636
637 #[test]
638 fn test_version_parse() {
639 let req = SolData::parse_version_req(">=0.6.2 <0.8.21").unwrap();
640 let semver_req: VersionReq = ">=0.6.2,<0.8.21".parse().unwrap();
641 assert_eq!(req, semver_req);
642 }
643
644 fn solc() -> Solc {
645 if let Some(solc) = Solc::find_svm_installed_version(&Version::new(0, 8, 18)).unwrap() {
646 solc
647 } else {
648 Solc::blocking_install(&Version::new(0, 8, 18)).unwrap()
649 }
650 }
651
652 #[test]
653 fn solc_version_works() {
654 Solc::version(solc().solc).unwrap();
655 }
656
657 #[test]
658 fn can_parse_version_metadata() {
659 let _version = Version::from_str("0.6.6+commit.6c089d02.Linux.gcc").unwrap();
660 }
661
662 #[cfg(feature = "async")]
663 #[tokio::test(flavor = "multi_thread")]
664 async fn async_solc_version_works() {
665 Solc::async_version(&solc().solc).await.unwrap();
666 }
667
668 #[test]
669 fn solc_compile_works() {
670 let input = include_str!("../../../../../test-data/in/compiler-in-1.json");
671 let input: SolcInput = serde_json::from_str(input).unwrap();
672 let out = solc().compile(&input).unwrap();
673 let other = solc().compile(&serde_json::json!(input)).unwrap();
674 assert_eq!(out, other);
675 }
676
677 #[test]
678 fn solc_metadata_works() {
679 let input = include_str!("../../../../../test-data/in/compiler-in-1.json");
680 let mut input: SolcInput = serde_json::from_str(input).unwrap();
681 input.settings.push_output_selection("metadata");
682 let out = solc().compile(&input).unwrap();
683 for (_, c) in out.split().1.contracts_iter() {
684 assert!(c.metadata.is_some());
685 }
686 }
687
688 #[test]
689 fn can_compile_with_remapped_links() {
690 let input: SolcInput = serde_json::from_str(include_str!(
691 "../../../../../test-data/library-remapping-in.json"
692 ))
693 .unwrap();
694 let out = solc().compile(&input).unwrap();
695 let (_, mut contracts) = out.split();
696 let contract = contracts.remove("LinkTest").unwrap();
697 let bytecode = &contract.get_bytecode().unwrap().object;
698 assert!(!bytecode.is_unlinked());
699 }
700
701 #[test]
702 fn can_compile_with_remapped_links_temp_dir() {
703 let input: SolcInput = serde_json::from_str(include_str!(
704 "../../../../../test-data/library-remapping-in-2.json"
705 ))
706 .unwrap();
707 let out = solc().compile(&input).unwrap();
708 let (_, mut contracts) = out.split();
709 let contract = contracts.remove("LinkTest").unwrap();
710 let bytecode = &contract.get_bytecode().unwrap().object;
711 assert!(!bytecode.is_unlinked());
712 }
713
714 #[cfg(feature = "async")]
715 #[tokio::test(flavor = "multi_thread")]
716 async fn async_solc_compile_works() {
717 let input = include_str!("../../../../../test-data/in/compiler-in-1.json");
718 let input: SolcInput = serde_json::from_str(input).unwrap();
719 let out = solc().async_compile(&input).await.unwrap();
720 let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
721 assert_eq!(out, other);
722 }
723
724 #[cfg(feature = "async")]
725 #[tokio::test(flavor = "multi_thread")]
726 async fn async_solc_compile_works2() {
727 let input = include_str!("../../../../../test-data/in/compiler-in-2.json");
728 let input: SolcInput = serde_json::from_str(input).unwrap();
729 let out = solc().async_compile(&input).await.unwrap();
730 let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
731 assert_eq!(out, other);
732 let sync_out = solc().compile(&input).unwrap();
733 assert_eq!(out, sync_out);
734 }
735
736 #[test]
737 fn test_version_req() {
738 let versions = ["=0.1.2", "^0.5.6", ">=0.7.1", ">0.8.0"];
739
740 versions.iter().for_each(|version| {
741 let version_req = SolData::parse_version_req(version).unwrap();
742 assert_eq!(version_req, VersionReq::from_str(version).unwrap());
743 });
744
745 let version_range = ">=0.8.0 <0.9.0";
748 let version_req = SolData::parse_version_req(version_range).unwrap();
749 assert_eq!(version_req, VersionReq::from_str(">=0.8.0,<0.9.0").unwrap());
750 }
751
752 #[test]
753 #[cfg(feature = "full")]
754 fn test_find_installed_version_path() {
755 take_solc_installer_lock!(_lock);
757 let version = Version::new(0, 8, 6);
758 if utils::installed_versions(svm::data_dir())
759 .map(|versions| !versions.contains(&version))
760 .unwrap_or_default()
761 {
762 Solc::blocking_install(&version).unwrap();
763 }
764 drop(_lock);
765 let res = Solc::find_svm_installed_version(&version).unwrap().unwrap();
766 let expected = svm::data_dir().join(version.to_string()).join(format!("solc-{version}"));
767 assert_eq!(res.solc, expected);
768 }
769
770 #[test]
771 #[cfg(feature = "svm-solc")]
772 fn can_install_solc_in_tokio_rt() {
773 let version = Version::from_str("0.8.6").unwrap();
774 let rt = tokio::runtime::Runtime::new().unwrap();
775 let result = rt.block_on(async { Solc::blocking_install(&version) });
776 assert!(result.is_ok());
777 }
778
779 #[test]
780 fn does_not_find_not_installed_version() {
781 let ver = Version::new(1, 1, 1);
782 let res = Solc::find_svm_installed_version(&ver).unwrap();
783 assert!(res.is_none());
784 }
785}