1use crate::resolver::parse::SolData;
2use foundry_compilers_artifacts::{sources::Source, CompilerOutput, SolcInput};
3use foundry_compilers_core::{
4 error::{Result, SolcError},
5 utils::{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 #[instrument(name = "Solc::new", skip_all)]
93 pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
94 Self::new_with_args(path, Vec::<String>::new())
95 }
96
97 pub fn new_with_args(
102 path: impl Into<PathBuf>,
103 extra_args: impl IntoIterator<Item: Into<String>>,
104 ) -> Result<Self> {
105 let path = path.into();
106 let extra_args = extra_args.into_iter().map(Into::into).collect::<Vec<_>>();
107 let version = Self::version_with_args(path.clone(), &extra_args)?;
108 Ok(Self::_new(path, version, extra_args))
109 }
110
111 pub fn new_with_version(path: impl Into<PathBuf>, version: Version) -> Self {
113 Self::_new(path.into(), version, Default::default())
114 }
115
116 fn _new(path: PathBuf, version: Version, extra_args: Vec<String>) -> Self {
117 let this = Self {
118 solc: path,
119 version,
120 base_path: None,
121 allow_paths: Default::default(),
122 include_paths: Default::default(),
123 extra_args,
124 };
125 this.debug_assert();
126 this
127 }
128
129 fn debug_assert(&self) {
130 if !cfg!(debug_assertions) {
131 return;
132 }
133 if let Ok(v) = Self::version_with_args(&self.solc, &self.extra_args) {
134 assert_eq!(v.major, self.version.major);
135 assert_eq!(v.minor, self.version.minor);
136 assert_eq!(v.patch, self.version.patch);
137 }
138 }
139
140 pub fn source_version_req(source: &Source) -> Result<VersionReq> {
143 Ok(SolData::parse_version_pragma(&source.content).ok_or(SolcError::PragmaNotFound)??)
144 }
145
146 #[cfg(feature = "svm-solc")]
151 pub fn detect_version(source: &Source) -> Result<Version> {
152 let sol_version = Self::source_version_req(source)?;
154 Self::ensure_installed(&sol_version)
155 }
156
157 #[cfg(feature = "svm-solc")]
162 pub fn ensure_installed(sol_version: &VersionReq) -> Result<Version> {
163 #[cfg(test)]
164 take_solc_installer_lock!(_lock);
165
166 let versions = Self::installed_versions();
168
169 let local_versions = Self::find_matching_installation(&versions, sol_version);
170 let remote_versions = Self::find_matching_installation(&RELEASES.1, sol_version);
171
172 Ok(match (local_versions, remote_versions) {
174 (Some(local), None) => local,
175 (Some(local), Some(remote)) => {
176 if remote > local {
177 Self::blocking_install(&remote)?;
178 remote
179 } else {
180 local
181 }
182 }
183 (None, Some(version)) => {
184 Self::blocking_install(&version)?;
185 version
186 }
187 _ => return Err(SolcError::VersionNotFound),
189 })
190 }
191
192 pub fn find_matching_installation(
195 versions: &[Version],
196 required_version: &VersionReq,
197 ) -> Option<Version> {
198 versions.iter().rev().find(|version| required_version.matches(version)).cloned()
200 }
201
202 #[instrument(skip_all)]
216 #[cfg(feature = "svm-solc")]
217 pub fn find_svm_installed_version(version: &Version) -> Result<Option<Self>> {
218 let version = if version.pre.is_empty() {
219 Version::new(version.major, version.minor, version.patch)
220 } else {
221 version.clone()
223 };
224 let solc = svm::version_binary(&version.to_string());
225 if !solc.is_file() {
226 return Ok(None);
227 }
228 Ok(Some(Self::new_with_version(&solc, version)))
229 }
230
231 #[cfg(feature = "svm-solc")]
237 pub fn svm_home() -> Option<PathBuf> {
238 Some(svm::data_dir().to_path_buf())
239 }
240
241 #[cfg(feature = "svm-solc")]
247 pub fn svm_global_version() -> Option<Version> {
248 svm::get_global_version().ok().flatten()
249 }
250
251 #[cfg(feature = "svm-solc")]
253 pub fn installed_versions() -> Vec<Version> {
254 svm::installed_versions().unwrap_or_default()
255 }
256
257 #[cfg(feature = "svm-solc")]
259 pub fn released_versions() -> Vec<Version> {
260 RELEASES.1.clone()
261 }
262
263 #[cfg(feature = "svm-solc")]
277 #[instrument(name = "Solc::install", skip_all)]
278 pub async fn install(version: &Version) -> std::result::Result<Self, svm::SvmError> {
279 trace!("installing solc version \"{}\"", version);
280 crate::report::solc_installation_start(version);
281 match svm::install(version).await {
282 Ok(path) => {
283 crate::report::solc_installation_success(version);
284 Ok(Self::new_with_version(path, version.clone()))
285 }
286 Err(err) => {
287 crate::report::solc_installation_error(version, &err.to_string());
288 Err(err)
289 }
290 }
291 }
292
293 #[cfg(feature = "svm-solc")]
295 #[instrument(name = "Solc::blocking_install", skip_all)]
296 pub fn blocking_install(version: &Version) -> std::result::Result<Self, svm::SvmError> {
297 use foundry_compilers_core::utils::RuntimeOrHandle;
298
299 #[cfg(test)]
300 crate::take_solc_installer_lock!(_lock);
301
302 let version = if version.pre.is_empty() {
303 Version::new(version.major, version.minor, version.patch)
304 } else {
305 version.clone()
307 };
308
309 trace!("blocking installing solc version \"{}\"", version);
310 crate::report::solc_installation_start(&version);
311 match RuntimeOrHandle::new().block_on(svm::install(&version)) {
315 Ok(path) => {
316 crate::report::solc_installation_success(&version);
317 Ok(Self::new_with_version(path, version.clone()))
318 }
319 Err(err) => {
320 crate::report::solc_installation_error(&version, &err.to_string());
321 Err(err)
322 }
323 }
324 }
325
326 #[cfg(feature = "svm-solc")]
329 #[instrument(name = "Solc::verify_checksum", skip_all)]
330 pub fn verify_checksum(&self) -> Result<()> {
331 let version = self.version_short();
332 let mut version_path = svm::version_path(version.to_string().as_str());
333 version_path.push(format!("solc-{}", version.to_string().as_str()));
334 trace!(target:"solc", "reading solc binary for checksum {:?}", version_path);
335 let content =
336 std::fs::read(&version_path).map_err(|err| SolcError::io(err, version_path.clone()))?;
337
338 if !RELEASES.2 {
339 return Ok(());
342 }
343
344 #[cfg(windows)]
345 {
346 const V0_7_2: Version = Version::new(0, 7, 2);
349 if version < V0_7_2 {
350 return Ok(());
351 }
352 }
353
354 use sha2::Digest;
355 let mut hasher = sha2::Sha256::new();
356 hasher.update(content);
357 let checksum_calc = &hasher.finalize()[..];
358
359 let checksum_found = &RELEASES
360 .0
361 .get_checksum(&version)
362 .ok_or_else(|| SolcError::ChecksumNotFound { version: version.clone() })?;
363
364 if checksum_calc == checksum_found {
365 Ok(())
366 } else {
367 use alloy_primitives::hex;
368 let expected = hex::encode(checksum_found);
369 let detected = hex::encode(checksum_calc);
370 warn!(target: "solc", "checksum mismatch for {:?}, expected {}, but found {} for file {:?}", version, expected, detected, version_path);
371 Err(SolcError::ChecksumMismatch { version, expected, detected, file: version_path })
372 }
373 }
374
375 pub fn compile_source(&self, path: &Path) -> Result<CompilerOutput> {
377 let mut res: CompilerOutput = Default::default();
378 for input in
379 SolcInput::resolve_and_build(Source::read_sol_yul_from(path)?, Default::default())
380 {
381 let input = input.sanitized(&self.version);
382 let output = self.compile(&input)?;
383 res.merge(output)
384 }
385
386 Ok(res)
387 }
388
389 pub fn compile_exact(&self, input: &SolcInput) -> Result<CompilerOutput> {
397 let mut out = self.compile(input)?;
398 out.retain_files(input.sources.keys().map(|p| p.as_path()));
399 Ok(out)
400 }
401
402 pub fn compile<T: Serialize>(&self, input: &T) -> Result<CompilerOutput> {
422 self.compile_as(input)
423 }
424
425 #[instrument(name = "Solc::compile", skip_all)]
427 pub fn compile_as<T: Serialize, D: DeserializeOwned>(&self, input: &T) -> Result<D> {
428 let output = self.compile_output(input)?;
429
430 let output = std::str::from_utf8(&output).map_err(|_| SolcError::InvalidUtf8)?;
432
433 Ok(serde_json::from_str(output)?)
434 }
435
436 #[instrument(name = "Solc::compile_raw", skip_all)]
438 pub fn compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
439 let mut cmd = self.configure_cmd();
440
441 trace!(input=%serde_json::to_string(input).unwrap_or_else(|e| e.to_string()));
442 debug!(?cmd, "compiling");
443
444 let mut child = cmd.spawn().map_err(self.map_io_err())?;
445 debug!("spawned");
446
447 {
448 let mut stdin = io::BufWriter::new(child.stdin.take().unwrap());
449 serde_json::to_writer(&mut stdin, input)?;
450 stdin.flush().map_err(self.map_io_err())?;
451 }
452 debug!("wrote JSON input to stdin");
453
454 let output = child.wait_with_output().map_err(self.map_io_err())?;
455 debug!(%output.status, output.stderr = ?String::from_utf8_lossy(&output.stderr), "finished");
456
457 compile_output(output)
458 }
459
460 pub fn version_short(&self) -> Version {
462 Version::new(self.version.major, self.version.minor, self.version.patch)
463 }
464
465 pub fn version(solc: impl Into<PathBuf>) -> Result<Version> {
467 Self::version_with_args(solc, &[])
468 }
469
470 pub fn version_with_args(solc: impl Into<PathBuf>, args: &[String]) -> Result<Version> {
472 crate::cache_version(solc.into(), args, |solc| Self::version_impl(solc, args))
473 }
474
475 fn version_impl(solc: &Path, args: &[String]) -> Result<Version> {
476 let mut cmd = Command::new(solc);
477 cmd.args(args)
478 .arg("--version")
479 .stdin(Stdio::piped())
480 .stderr(Stdio::piped())
481 .stdout(Stdio::piped());
482 debug!(?cmd, "getting Solc version");
483 let output = cmd.output().map_err(|e| SolcError::io(e, solc))?;
484 trace!(?output);
485 let version = version_from_output(output)?;
486 debug!(%version);
487 Ok(version)
488 }
489
490 fn map_io_err(&self) -> impl FnOnce(std::io::Error) -> SolcError + '_ {
491 move |err| SolcError::io(err, &self.solc)
492 }
493
494 pub fn configure_cmd(&self) -> Command {
498 let mut cmd = Command::new(&self.solc);
499 cmd.stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped());
500 cmd.args(&self.extra_args);
501
502 if !self.allow_paths.is_empty() {
503 cmd.arg("--allow-paths");
504 cmd.arg(self.allow_paths.iter().map(|p| p.display()).join(","));
505 }
506 if let Some(base_path) = &self.base_path {
507 if SUPPORTS_BASE_PATH.matches(&self.version) {
508 if SUPPORTS_INCLUDE_PATH.matches(&self.version) {
509 for path in
513 self.include_paths.iter().filter(|p| p.as_path() != base_path.as_path())
514 {
515 cmd.arg("--include-path").arg(path);
516 }
517 }
518
519 cmd.arg("--base-path").arg(base_path);
520 }
521
522 cmd.current_dir(base_path);
523 }
524
525 cmd.arg("--standard-json");
526
527 cmd
528 }
529
530 #[cfg(feature = "svm-solc")]
532 pub fn find_or_install(version: &Version) -> Result<Self> {
533 let solc = if let Some(solc) = Self::find_svm_installed_version(version)? {
534 solc
535 } else {
536 Self::blocking_install(version)?
537 };
538
539 Ok(solc)
540 }
541}
542
543#[cfg(feature = "async")]
544impl Solc {
545 pub async fn async_compile_source(&self, path: &Path) -> Result<CompilerOutput> {
547 self.async_compile(&SolcInput::resolve_and_build(
548 Source::async_read_all_from(path, SOLC_EXTENSIONS).await?,
549 Default::default(),
550 ))
551 .await
552 }
553
554 pub async fn async_compile<T: Serialize>(&self, input: &T) -> Result<CompilerOutput> {
557 self.async_compile_as(input).await
558 }
559
560 pub async fn async_compile_as<T: Serialize, D: DeserializeOwned>(
563 &self,
564 input: &T,
565 ) -> Result<D> {
566 let output = self.async_compile_output(input).await?;
567 Ok(serde_json::from_slice(&output)?)
568 }
569
570 pub async fn async_compile_output<T: Serialize>(&self, input: &T) -> Result<Vec<u8>> {
571 use tokio::{io::AsyncWriteExt, process::Command};
572
573 let mut cmd: Command = self.configure_cmd().into();
574 let mut child = cmd.spawn().map_err(self.map_io_err())?;
575 let stdin = child.stdin.as_mut().unwrap();
576
577 let content = serde_json::to_vec(input)?;
578
579 stdin.write_all(&content).await.map_err(self.map_io_err())?;
580 stdin.flush().await.map_err(self.map_io_err())?;
581
582 compile_output(child.wait_with_output().await.map_err(self.map_io_err())?)
583 }
584
585 pub async fn async_version(solc: &Path) -> Result<Version> {
586 let mut cmd = tokio::process::Command::new(solc);
587 cmd.arg("--version").stdin(Stdio::piped()).stderr(Stdio::piped()).stdout(Stdio::piped());
588 debug!(?cmd, "getting version");
589 let output = cmd.output().await.map_err(|e| SolcError::io(e, solc))?;
590 let version = version_from_output(output)?;
591 debug!(%version);
592 Ok(version)
593 }
594
595 pub async fn compile_many<I>(jobs: I, n: usize) -> crate::many::CompiledMany
601 where
602 I: IntoIterator<Item = (Self, SolcInput)>,
603 {
604 use futures_util::stream::StreamExt;
605
606 let outputs = futures_util::stream::iter(
607 jobs.into_iter()
608 .map(|(solc, input)| async { (solc.async_compile(&input).await, solc, input) }),
609 )
610 .buffer_unordered(n)
611 .collect::<Vec<_>>()
612 .await;
613
614 crate::many::CompiledMany::new(outputs)
615 }
616}
617
618fn compile_output(output: Output) -> Result<Vec<u8>> {
619 if output.status.success() {
620 Ok(output.stdout)
621 } else {
622 Err(SolcError::solc_output(&output))
623 }
624}
625
626fn version_from_output(output: Output) -> Result<Version> {
627 if output.status.success() {
628 let stdout = String::from_utf8_lossy(&output.stdout);
629 let version = stdout
630 .lines()
631 .rfind(|l| !l.trim().is_empty())
632 .ok_or_else(|| SolcError::msg("Version not found in Solc output"))?;
633 Ok(Version::from_str(&version.trim_start_matches("Version: ").replace(".g++", ".gcc"))?)
635 } else {
636 Err(SolcError::solc_output(&output))
637 }
638}
639
640impl AsRef<Path> for Solc {
641 fn as_ref(&self) -> &Path {
642 &self.solc
643 }
644}
645
646#[cfg(test)]
647#[cfg(feature = "svm-solc")]
648mod tests {
649 use super::*;
650 use crate::{resolver::parse::SolData, Artifact};
651
652 #[test]
653 fn test_version_parse() {
654 let req = SolData::parse_version_req(">=0.6.2 <0.8.21").unwrap();
655 let semver_req: VersionReq = ">=0.6.2,<0.8.21".parse().unwrap();
656 assert_eq!(req, semver_req);
657 }
658
659 fn solc() -> Solc {
660 if let Some(solc) = Solc::find_svm_installed_version(&Version::new(0, 8, 18)).unwrap() {
661 solc
662 } else {
663 Solc::blocking_install(&Version::new(0, 8, 18)).unwrap()
664 }
665 }
666
667 #[test]
668 fn solc_version_works() {
669 Solc::version(solc().solc).unwrap();
670 }
671
672 #[test]
673 fn can_parse_version_metadata() {
674 let _version = Version::from_str("0.6.6+commit.6c089d02.Linux.gcc").unwrap();
675 }
676
677 #[cfg(feature = "async")]
678 #[tokio::test(flavor = "multi_thread")]
679 async fn async_solc_version_works() {
680 Solc::async_version(&solc().solc).await.unwrap();
681 }
682
683 #[test]
684 fn solc_compile_works() {
685 let input = include_str!("../../../../../test-data/in/compiler-in-1.json");
686 let input: SolcInput = serde_json::from_str(input).unwrap();
687 let out = solc().compile(&input).unwrap();
688 let other = solc().compile(&serde_json::json!(input)).unwrap();
689 assert_eq!(out, other);
690 }
691
692 #[test]
693 fn solc_metadata_works() {
694 let input = include_str!("../../../../../test-data/in/compiler-in-1.json");
695 let mut input: SolcInput = serde_json::from_str(input).unwrap();
696 input.settings.push_output_selection("metadata");
697 let out = solc().compile(&input).unwrap();
698 for (_, c) in out.split().1.contracts_iter() {
699 assert!(c.metadata.is_some());
700 }
701 }
702
703 #[test]
704 fn can_compile_with_remapped_links() {
705 let input: SolcInput = serde_json::from_str(include_str!(
706 "../../../../../test-data/library-remapping-in.json"
707 ))
708 .unwrap();
709 let out = solc().compile(&input).unwrap();
710 let (_, mut contracts) = out.split();
711 let contract = contracts.remove("LinkTest").unwrap();
712 let bytecode = &contract.get_bytecode().unwrap().object;
713 assert!(!bytecode.is_unlinked());
714 }
715
716 #[test]
717 fn can_compile_with_remapped_links_temp_dir() {
718 let input: SolcInput = serde_json::from_str(include_str!(
719 "../../../../../test-data/library-remapping-in-2.json"
720 ))
721 .unwrap();
722 let out = solc().compile(&input).unwrap();
723 let (_, mut contracts) = out.split();
724 let contract = contracts.remove("LinkTest").unwrap();
725 let bytecode = &contract.get_bytecode().unwrap().object;
726 assert!(!bytecode.is_unlinked());
727 }
728
729 #[cfg(feature = "async")]
730 #[tokio::test(flavor = "multi_thread")]
731 async fn async_solc_compile_works() {
732 let input = include_str!("../../../../../test-data/in/compiler-in-1.json");
733 let input: SolcInput = serde_json::from_str(input).unwrap();
734 let out = solc().async_compile(&input).await.unwrap();
735 let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
736 assert_eq!(out, other);
737 }
738
739 #[cfg(feature = "async")]
740 #[tokio::test(flavor = "multi_thread")]
741 async fn async_solc_compile_works2() {
742 let input = include_str!("../../../../../test-data/in/compiler-in-2.json");
743 let input: SolcInput = serde_json::from_str(input).unwrap();
744 let out = solc().async_compile(&input).await.unwrap();
745 let other = solc().async_compile(&serde_json::json!(input)).await.unwrap();
746 assert_eq!(out, other);
747 let sync_out = solc().compile(&input).unwrap();
748 assert_eq!(out, sync_out);
749 }
750
751 #[test]
752 fn test_version_req() {
753 let versions = ["=0.1.2", "^0.5.6", ">=0.7.1", ">0.8.0"];
754
755 versions.iter().for_each(|version| {
756 let version_req = SolData::parse_version_req(version).unwrap();
757 assert_eq!(version_req, VersionReq::from_str(version).unwrap());
758 });
759
760 let version_range = ">=0.8.0 <0.9.0";
763 let version_req = SolData::parse_version_req(version_range).unwrap();
764 assert_eq!(version_req, VersionReq::from_str(">=0.8.0,<0.9.0").unwrap());
765 }
766
767 #[test]
768 #[cfg(feature = "full")]
769 fn test_find_installed_version_path() {
770 take_solc_installer_lock!(_lock);
772 let version = Version::new(0, 8, 6);
773 if svm::installed_versions()
774 .map(|versions| !versions.contains(&version))
775 .unwrap_or_default()
776 {
777 Solc::blocking_install(&version).unwrap();
778 }
779 drop(_lock);
780 let res = Solc::find_svm_installed_version(&version).unwrap().unwrap();
781 let expected = svm::data_dir().join(version.to_string()).join(format!("solc-{version}"));
782 assert_eq!(res.solc, expected);
783 }
784
785 #[test]
786 #[cfg(feature = "svm-solc")]
787 fn can_install_solc_in_tokio_rt() {
788 let version = Version::from_str("0.8.6").unwrap();
789 let rt = tokio::runtime::Runtime::new().unwrap();
790 let result = rt.block_on(async { Solc::blocking_install(&version) });
791 assert!(result.is_ok());
792 }
793
794 #[test]
795 fn does_not_find_not_installed_version() {
796 let ver = Version::new(1, 1, 1);
797 let res = Solc::find_svm_installed_version(&ver).unwrap();
798 assert!(res.is_none());
799 }
800}