Skip to main content

forge/cmd/
bind.rs

1use alloy_json_abi::ToSolConfig;
2use alloy_primitives::map::{HashMap, HashSet, hash_map::Entry};
3use clap::{Parser, ValueHint};
4use eyre::Result;
5use forge_sol_macro_gen::{MultiSolMacroGen, SolMacroGen};
6use foundry_cli::{opts::BuildOpts, utils::LoadConfig};
7use foundry_common::{
8    compile::{ProjectCompiler, compile_abi_project},
9    fs::json_files,
10};
11use foundry_compilers::{
12    Graph, Project,
13    artifacts::{Contract, output_selection::OutputSelection},
14    cache::CompilerCache,
15    compilers::{Compiler, CompilerOutput, CompilerSettings, CompilerVersion},
16    error::SolcError,
17    multi::{MultiCompiler, MultiCompilerParser, MultiCompilerSettings},
18};
19use foundry_config::{Config, impl_figment_convert};
20use heck::ToSnakeCase;
21use regex::Regex;
22use semver::Version;
23use solar::ast::{Item, ItemKind};
24use std::{
25    collections::{BTreeMap, BTreeSet},
26    fs,
27    path::{Path, PathBuf},
28};
29
30impl_figment_convert!(BindArgs, build);
31
32const DEFAULT_CRATE_NAME: &str = "foundry-contracts";
33const DEFAULT_CRATE_VERSION: &str = "0.1.0";
34
35type BindingFiles = Vec<BindingFile>;
36
37struct BindingFile {
38    /// Original contract name, without compiler version or profile suffixes.
39    name: String,
40    path: PathBuf,
41    /// Source identity, if available. Otherwise the artifact directory identifies it.
42    source: PathBuf,
43}
44
45/// CLI arguments for `forge bind`.
46#[derive(Clone, Debug, Parser)]
47pub struct BindArgs {
48    /// Path to where the contract artifacts are stored.
49    #[arg(
50        long = "bindings-path",
51        short,
52        value_hint = ValueHint::DirPath,
53        value_name = "PATH"
54    )]
55    pub bindings: Option<PathBuf>,
56
57    /// Create bindings only for contracts whose names match the specified filter(s)
58    #[arg(long)]
59    pub select: Vec<regex::Regex>,
60
61    /// Explicitly generate bindings for all contracts
62    ///
63    /// By default all contracts ending with `Test` or `Script` are excluded.
64    #[arg(long, conflicts_with_all = &["select", "skip"])]
65    pub select_all: bool,
66
67    /// The name of the Rust crate to generate.
68    ///
69    /// This should be a valid crates.io crate name,
70    /// however, this is not currently validated by this command.
71    #[arg(long, default_value = DEFAULT_CRATE_NAME, value_name = "NAME")]
72    crate_name: String,
73
74    /// The version of the Rust crate to generate.
75    ///
76    /// This should be a standard semver version string,
77    /// however, this is not currently validated by this command.
78    #[arg(long, default_value = DEFAULT_CRATE_VERSION, value_name = "VERSION")]
79    crate_version: String,
80
81    /// The description of the Rust crate to generate.
82    ///
83    /// This will be added to the package.description field in Cargo.toml.
84    #[arg(long, default_value = "", value_name = "DESCRIPTION")]
85    crate_description: String,
86
87    /// The license of the Rust crate to generate.
88    ///
89    /// This will be added to the package.license field in Cargo.toml.
90    #[arg(long, value_name = "LICENSE", default_value = "")]
91    crate_license: String,
92
93    /// Generate the bindings as a module instead of a crate.
94    #[arg(long)]
95    module: bool,
96
97    /// Overwrite existing generated bindings.
98    ///
99    /// By default, the command will check that the bindings are correct, and then exit. If
100    /// --overwrite is passed, it will instead delete and overwrite the bindings.
101    #[arg(long)]
102    overwrite: bool,
103
104    /// Generate bindings as a single file.
105    #[arg(long)]
106    single_file: bool,
107
108    /// Skip Cargo.toml consistency checks.
109    #[arg(long)]
110    skip_cargo_toml: bool,
111
112    /// Skips running forge build before generating binding
113    #[arg(long)]
114    skip_build: bool,
115
116    /// Don't add any additional derives to generated bindings
117    #[arg(long)]
118    skip_extra_derives: bool,
119
120    /// Generate bindings for the `alloy` library, instead of `ethers`.
121    #[arg(long, hide = true)]
122    alloy: bool,
123
124    /// Specify the `alloy` version on Crates.
125    #[arg(long)]
126    alloy_version: Option<String>,
127
128    /// Specify the `alloy` revision on GitHub.
129    #[arg(long, conflicts_with = "alloy_version")]
130    alloy_rev: Option<String>,
131
132    /// Generate bindings for the `ethers` library (removed), instead of `alloy`.
133    #[arg(long, hide = true)]
134    ethers: bool,
135
136    #[command(flatten)]
137    build: BuildOpts,
138}
139
140impl BindArgs {
141    pub fn run(self) -> Result<()> {
142        if self.ethers {
143            eyre::bail!("`--ethers` bindings have been removed. Use `--alloy` (default) instead.");
144        }
145
146        let config = if self.skip_build {
147            self.load_config()?
148        } else {
149            self.load_config_with_dependencies()?
150        };
151        let artifacts = config.out.clone();
152        let (files, enum_definitions) = if self.skip_build {
153            self.cached_bindings(&config)?
154        } else {
155            let mut project = config.project()?;
156            // Compilation may install compiler versions and change subsequent auto-detection.
157            let contexts = resolved_artifact_contexts(&project, artifact_graph(&project, None)?)?;
158            let output = compile_abi_project(
159                &mut project,
160                ProjectCompiler::new().external_compilers(&config),
161            )?;
162            let files =
163                self.filter_artifact_paths(output.artifact_ids().filter_map(|(id, _)| {
164                    (id.path.starts_with(artifacts.join(".external"))
165                        || contexts
166                            .get(&id.source)
167                            .is_some_and(|contexts| contexts.contains(&(id.version, id.profile))))
168                    .then_some((id.path, Some(id.source)))
169                }))?;
170            (files, enum_definitions(output.parser()))
171        };
172
173        let bindings_root = self.bindings.clone().unwrap_or_else(|| artifacts.join("bindings"));
174        let sol_config = ToSolConfig::new().enum_definitions(enum_definitions);
175        // Validate identities and names before overwriting any existing bindings.
176        let mut bindings = self.get_solmacrogen(&files)?;
177
178        if bindings_root.exists() {
179            if !self.overwrite {
180                sh_status!("Bindings found. Checking for consistency.")?;
181                bindings.generate_bindings(!self.skip_extra_derives, &sol_config)?;
182                return self.check_existing_bindings(&bindings, &bindings_root);
183            }
184
185            trace!(?artifacts, "Removing existing bindings");
186            fs::remove_dir_all(&bindings_root)?;
187        }
188
189        self.generate_bindings(&mut bindings, &bindings_root, &sol_config)?;
190
191        sh_status!("Bindings have been generated to {}", bindings_root.display())?;
192        Ok(())
193    }
194
195    fn get_filter(&self) -> Result<Filter> {
196        if self.select_all {
197            // Select all json files
198            return Ok(Filter::All);
199        }
200        if !self.select.is_empty() {
201            // Return json files that match the select regex
202            return Ok(Filter::Select(self.select.clone()));
203        }
204
205        if let Some(skip) = self.build.skip.as_ref().filter(|s| !s.is_empty()) {
206            return Ok(Filter::Skip(
207                skip.clone()
208                    .into_iter()
209                    .map(|s| Regex::new(s.file_pattern()))
210                    .collect::<Result<Vec<_>, _>>()?,
211            ));
212        }
213
214        // Exclude defaults
215        Ok(Filter::skip_default())
216    }
217
218    /// Selects cached artifacts and enriches enums only from a fresh, complete source graph.
219    fn cached_bindings(
220        &self,
221        config: &Config,
222    ) -> Result<(BindingFiles, BTreeMap<String, Vec<String>>)> {
223        let paths = config.project_paths();
224        let cache = CompilerCache::<MultiCompilerSettings>::read_joined(&paths).ok();
225        let graph = Graph::<MultiCompilerParser>::resolve(&paths);
226        let mut definitions = if let (Some(cache), Ok(graph)) = (&cache, &graph)
227            && cache.all_artifacts_exist()
228            && graph.nodes.iter().all(|node| {
229                cache
230                    .entry(node.path())
231                    .is_some_and(|entry| entry.content_hash == node.unpack().1.content_hash())
232            }) {
233            enum_definitions(graph.parser())
234        } else {
235            BTreeMap::default()
236        };
237        let files = if let Some(cache) = cache.as_ref().filter(|_| config.cache) {
238            let mut files = self.cached_artifact_paths(config, cache, graph)?;
239            // External adapter artifacts are not part of the built-in compiler cache.
240            files.extend(json_files(&config.out.join(".external")));
241            files
242        } else {
243            // Without cache-based selection, include all artifacts on disk.
244            json_files(&config.out).collect()
245        };
246        // Source identity is independent of cache-based selection: compiler variants can live in
247        // different artifact directories, even with --no-cache.
248        let sources = cache
249            .iter()
250            .flat_map(|cache| &cache.files)
251            .flat_map(|(source, entry)| {
252                entry.artifacts().map(move |artifact| (&artifact.path, source))
253            })
254            .collect::<HashMap<_, _>>();
255        let files = self.filter_artifact_paths(files.into_iter().map(|path| {
256            let source = sources.get(&path).map(|source| (*source).clone());
257            (path, source)
258        }))?;
259        // Cache-based selection already guarantees membership for built-in artifacts.
260        if !config.cache
261            && !definitions.is_empty()
262            && files.iter().any(|file| {
263                !file.path.starts_with(config.out.join(".external"))
264                    && !sources.contains_key(&file.path)
265            })
266        {
267            definitions.clear();
268        }
269        Ok((files, definitions))
270    }
271
272    /// Resolves built-in cached artifacts without compiling or changing compiler installations.
273    fn cached_artifact_paths(
274        &self,
275        config: &Config,
276        cache: &CompilerCache<MultiCompilerSettings>,
277        graph: Result<Graph<MultiCompilerParser>, SolcError>,
278    ) -> Result<Vec<PathBuf>> {
279        let filter = self.get_filter()?;
280        let selected = cache
281            .files
282            .iter()
283            .filter(|(_, entry)| {
284                entry.artifacts.keys().any(|name| filter.is_match(&binding_name(name)))
285            })
286            .map(|(source, _)| source.clone())
287            .collect::<HashSet<_>>();
288        if selected.is_empty() {
289            return Ok(Vec::new());
290        }
291        let mut files = Vec::new();
292        let compiler = CachedCompiler::new(config, cache);
293        let mut project = config.create_project_with_compiler(|| Ok(compiler), false, true)?;
294        project.update_output_selection(|selection| {
295            *selection = OutputSelection::common_output_selection(["abi".to_string()]);
296        });
297        // Enum metadata always uses the full graph; artifact selection may be narrower.
298        let graph = if project.sparse_output.is_none() {
299            graph.map_err(Into::into)
300        } else {
301            artifact_graph(&project, None)
302        };
303        // Preserve shared compiler versions, falling back to selected inputs if needed.
304        let contexts =
305            graph.and_then(|graph| resolved_artifact_contexts(&project, graph)).or_else(|_| {
306                let graph = artifact_graph(&project, Some(&selected))?;
307                resolved_artifact_contexts(&project, graph)
308            })?;
309        let compatible_profiles = project
310            .settings_profiles()
311            .filter_map(|(profile, settings)| {
312                cache
313                    .profiles
314                    .get(profile)
315                    .is_some_and(|cached| settings.can_use_cached(cached))
316                    .then_some(profile)
317            })
318            .collect::<HashSet<_>>();
319        for (source, contexts) in contexts {
320            if let Some(entry) = cache.entry(&source) {
321                let vyper =
322                    matches!(source.extension().and_then(|s| s.to_str()), Some("vy" | "vyi"));
323                let (unavailable, compiler, hint) = if vyper {
324                    (project.compiler.vyper_unknown, "Vyper", "configure `vyper.path`")
325                } else {
326                    (project.compiler.solc_unknown, "configured solc", "specify `--use <version>`")
327                };
328                for (name, versions) in
329                    entry.artifacts.iter().filter(|(name, _)| filter.is_match(&binding_name(name)))
330                {
331                    eyre::ensure!(
332                        !unavailable || versions.len() <= 1,
333                        "Cannot determine compiler version for {}:{name}: {compiler} is unavailable and multiple versions are cached; {hint}",
334                        source.display(),
335                    );
336                    for (version, profile) in &contexts {
337                        let artifact = entry.find_artifact(name, version, profile)
338                            .filter(|artifact| artifact.path.is_file())
339                            .ok_or_else(|| eyre::eyre!(
340                                "No cached artifact for {}:{name} with compiler {version} and profile `{profile}`; run `forge build` first",
341                                source.display(),
342                            ))?;
343                        eyre::ensure!(
344                            compatible_profiles.contains(profile.as_str()),
345                            "Cached compiler settings for {}:{name} and profile `{profile}` do not match; run `forge build` first",
346                            source.display(),
347                        );
348                        files.push(artifact.path.clone());
349                    }
350                }
351            }
352        }
353        Ok(files)
354    }
355
356    /// Applies binding filters and sorts variants of the same contract by artifact path.
357    fn filter_artifact_paths(
358        &self,
359        paths: impl IntoIterator<Item = (PathBuf, Option<PathBuf>)>,
360    ) -> Result<BindingFiles> {
361        let filter = self.get_filter()?;
362        let mut files = paths
363            .into_iter()
364            .filter_map(|(path, source)| {
365                if path.to_str()?.contains("build-info")
366                    || path.iter().any(|component| component == "target")
367                {
368                    return None;
369                }
370                let stem = path.file_stem()?.to_str()?;
371                if stem.ends_with(".metadata") {
372                    return None;
373                }
374                let name = stem.split('.').next().unwrap().to_owned();
375                filter.is_match(&binding_name(&name)).then(|| BindingFile {
376                    name,
377                    source: source.unwrap_or_else(|| path.parent().unwrap().to_path_buf()),
378                    path,
379                })
380            })
381            .collect::<Vec<_>>();
382        files.sort_by(|a, b| a.path.cmp(&b.path));
383        Ok(files)
384    }
385
386    fn get_solmacrogen(&self, files: &[BindingFile]) -> Result<MultiSolMacroGen> {
387        let mut identities = HashSet::<_>::default();
388        let mut names = HashMap::<_, &BindingFile>::default();
389        let mut modules = HashMap::<_, _>::default();
390        let mut instances = Vec::new();
391        for file in files {
392            if !identities.insert((&file.source, &file.name)) {
393                continue;
394            }
395            let name = binding_name(&file.name);
396            match names.entry(name.clone()) {
397                Entry::Occupied(entry) => {
398                    let previous = entry.get();
399                    sh_warn!(
400                        "Binding name collision for `{name}`: keeping contract `{}` ({}) and skipping contract `{}` ({})",
401                        previous.name,
402                        previous.path.display(),
403                        file.name,
404                        file.path.display(),
405                    )?;
406                    continue;
407                }
408                Entry::Vacant(entry) => {
409                    entry.insert(file);
410                }
411            }
412            if !self.single_file {
413                let module = name.to_snake_case();
414                let entrypoint = if self.module { "mod" } else { "lib" };
415                eyre::ensure!(
416                    module != entrypoint,
417                    "Binding module name collision for `{module}`: contract `{}` ({}) conflicts with generated entry point `{entrypoint}.rs`",
418                    file.name,
419                    file.path.display(),
420                );
421                if let Some(previous) = modules.insert(module.clone(), file) {
422                    eyre::bail!(
423                        "Binding module name collision for `{module}`: contract `{}` ({}) and contract `{}` ({})",
424                        previous.name,
425                        previous.path.display(),
426                        file.name,
427                        file.path.display(),
428                    );
429                }
430            }
431            trace!(path = ?file.path, "parsing SolMacroGen from file");
432            instances.push(SolMacroGen::new(file.path.clone(), name));
433        }
434
435        let multi = MultiSolMacroGen::new(instances);
436        eyre::ensure!(!multi.instances.is_empty(), "No contract artifacts found");
437        Ok(multi)
438    }
439
440    /// Check that the existing bindings match the expected abigen output
441    fn check_existing_bindings(
442        &self,
443        bindings: &MultiSolMacroGen,
444        bindings_root: &Path,
445    ) -> Result<()> {
446        sh_status!("Checking bindings for {} contracts", bindings.instances.len())?;
447        bindings.check_consistency(
448            &self.crate_name,
449            &self.crate_version,
450            bindings_root,
451            self.single_file,
452            !self.skip_cargo_toml,
453            self.module,
454            self.alloy_version.clone(),
455            self.alloy_rev.clone(),
456        )?;
457        sh_status!("OK.")?;
458        Ok(())
459    }
460
461    /// Generate the bindings
462    fn generate_bindings(
463        &self,
464        bindings: &mut MultiSolMacroGen,
465        bindings_root: &Path,
466        sol_config: &ToSolConfig,
467    ) -> Result<()> {
468        sh_status!("Generating bindings for {} contracts", bindings.instances.len())?;
469
470        if self.module {
471            trace!(single_file = self.single_file, "generating module");
472            bindings.write_to_module(
473                bindings_root,
474                self.single_file,
475                !self.skip_extra_derives,
476                sol_config,
477            )?;
478        } else {
479            trace!(single_file = self.single_file, "generating crate");
480            bindings.write_to_crate(
481                &self.crate_name,
482                &self.crate_version,
483                &self.crate_description,
484                &self.crate_license,
485                bindings_root,
486                self.single_file,
487                self.alloy_version.clone(),
488                self.alloy_rev.clone(),
489                !self.skip_extra_derives,
490                sol_config,
491            )?;
492        }
493
494        Ok(())
495    }
496}
497
498fn enum_definitions(parser: &MultiCompilerParser) -> BTreeMap<String, Vec<String>> {
499    parser.solc().compiler().enter(|compiler| {
500        let mut definitions = BTreeMap::default();
501        let mut ambiguous = HashSet::default();
502        for source in compiler.sources().iter() {
503            if let Some(ast) = &source.ast {
504                collect_enum_definitions(ast.items.iter(), None, &mut definitions, &mut ambiguous);
505            }
506        }
507        definitions
508    })
509}
510
511fn collect_enum_definitions<'ast>(
512    items: impl Iterator<Item = &'ast Item<'ast>>,
513    owner: Option<&str>,
514    definitions: &mut BTreeMap<String, Vec<String>>,
515    ambiguous: &mut HashSet<String>,
516) {
517    for item in items {
518        match &item.kind {
519            ItemKind::Enum(enum_item) => {
520                let name = enum_item.name.to_string();
521                let key = owner.map_or_else(|| name.clone(), |owner| format!("{owner}.{name}"));
522                let variants = enum_item.variants.iter().map(ToString::to_string).collect();
523                if definitions.get(&key).is_some_and(|existing| existing != &variants) {
524                    definitions.remove(&key);
525                    ambiguous.insert(key);
526                } else if !ambiguous.contains(&key) {
527                    definitions.insert(key, variants);
528                }
529            }
530            ItemKind::Contract(contract) => {
531                let owner = contract.name.to_string();
532                collect_enum_definitions(
533                    contract.body.iter(),
534                    Some(&owner),
535                    definitions,
536                    ambiguous,
537                );
538            }
539            _ => {}
540        }
541    }
542}
543
544pub enum Filter {
545    All,
546    Select(Vec<regex::Regex>),
547    Skip(Vec<regex::Regex>),
548}
549
550impl Filter {
551    pub fn is_match(&self, name: &str) -> bool {
552        match self {
553            Self::All => true,
554            Self::Select(regexes) => regexes.iter().any(|regex| regex.is_match(name)),
555            Self::Skip(regexes) => !regexes.iter().any(|regex| regex.is_match(name)),
556        }
557    }
558
559    pub fn skip_default() -> Self {
560        let skip = [
561            ".*Test.*",
562            ".*Script",
563            "console[2]?",
564            "CommonBase",
565            "Components",
566            "[Ss]td(Chains|Math|Error|Json|Utils|Cheats|Style|Invariant|Assertions|Toml|Storage(Safe)?)",
567            "[Vv]m.*",
568            "IMulticall3",
569        ]
570        .iter()
571        .map(|pattern| regex::Regex::new(pattern).unwrap())
572        .collect::<Vec<_>>();
573
574        Self::Skip(skip)
575    }
576}
577
578/// Resolves compilation inputs, retaining importers of selected library dependencies.
579fn artifact_graph<C: Compiler<CompilerContract = Contract>>(
580    project: &Project<C>,
581    selected: Option<&HashSet<PathBuf>>,
582) -> Result<Graph<C::Parser>> {
583    let mut sources = project.paths.read_input_files()?;
584    if let Some(filter) = &project.sparse_output {
585        sources.retain(|source, _| filter.is_match(source));
586    }
587    if let Some(selected) = selected {
588        let dependencies = selected
589            .iter()
590            .filter(|source| !sources.contains_key(*source))
591            .map(PathBuf::as_path)
592            .collect::<HashSet<_>>();
593        let mut roots = selected.clone();
594        if !dependencies.is_empty() {
595            // Libraries inherit their importers' compiler versions and profiles. Keep those
596            // inputs instead of treating selected libraries as standalone compilation roots.
597            let graph = Graph::<C::Parser>::resolve_sources(&project.paths, sources.clone())?;
598            roots.extend(
599                graph
600                    .input_nodes()
601                    .filter(|node| {
602                        graph
603                            .imports(node.path())
604                            .iter()
605                            .any(|import| dependencies.contains(import))
606                    })
607                    .map(|node| node.path().to_path_buf()),
608            );
609        }
610        sources.retain(|source, _| roots.contains(source));
611    }
612    Ok(Graph::resolve_sources(&project.paths, sources)?)
613}
614
615/// Resolves source/compiler/profile identities using the same graph as compilation.
616fn resolved_artifact_contexts<C: Compiler<CompilerContract = Contract>>(
617    project: &Project<C>,
618    graph: Graph<C::Parser>,
619) -> Result<BTreeMap<PathBuf, BTreeSet<(Version, String)>>> {
620    let resolved = graph.into_sources_by_version(project)?;
621    let mut contexts = BTreeMap::<_, BTreeSet<_>>::new();
622    for (version, sources, (profile, _)) in resolved.sources.into_values().flatten() {
623        for source in sources.keys() {
624            if resolved.primary_profiles.get(source).is_none_or(|primary| *primary == profile) {
625                contexts
626                    .entry(source.clone())
627                    .or_default()
628                    .insert((version.clone(), profile.to_string()));
629            }
630        }
631    }
632    Ok(contexts)
633}
634
635fn binding_name(name: &str) -> String {
636    name.replace(char::is_whitespace, "").replace(['-', '$'], "_")
637}
638
639/// Provides compiler versions to the graph without installing or compiling anything.
640#[derive(Clone)]
641struct CachedCompiler {
642    solc: Vec<CompilerVersion>,
643    vyper: Vec<CompilerVersion>,
644    solc_unknown: bool,
645    vyper_unknown: bool,
646}
647
648impl CachedCompiler {
649    fn new(config: &Config, cache: &CompilerCache<MultiCompilerSettings>) -> Self {
650        let cached_versions = |vyper| {
651            cache
652                .files
653                .iter()
654                .filter(|(source, _)| {
655                    matches!(source.extension().and_then(|s| s.to_str()), Some("vy" | "vyi"))
656                        == vyper
657                })
658                .flat_map(|(_, entry)| {
659                    entry.artifacts.values().flat_map(|versions| versions.keys().cloned())
660                })
661                .collect::<BTreeSet<_>>()
662                .into_iter()
663                .map(CompilerVersion::Installed)
664                .collect::<Vec<_>>()
665        };
666        let mut solc_unknown = false;
667        let solc = match &config.solc {
668            Some(request) => {
669                if let Ok(version) = request.try_version() {
670                    vec![CompilerVersion::Installed(Version::new(
671                        version.major,
672                        version.minor,
673                        version.patch,
674                    ))]
675                } else {
676                    solc_unknown = true;
677                    cached_versions(false)
678                }
679            }
680            // Cached bindings must not change when another compiler is installed after the build.
681            None => cached_versions(false),
682        };
683        let mut vyper_unknown = false;
684        let vyper = if let Ok(Some(compiler)) = config.vyper_compiler() {
685            compiler.available_versions(&Default::default())
686        } else {
687            vyper_unknown = true;
688            cached_versions(true)
689        };
690        Self { solc, vyper, solc_unknown, vyper_unknown }
691    }
692}
693
694impl Compiler for CachedCompiler {
695    type Input = <MultiCompiler as Compiler>::Input;
696    type CompilationError = <MultiCompiler as Compiler>::CompilationError;
697    type CompilerContract = <MultiCompiler as Compiler>::CompilerContract;
698    type Parser = <MultiCompiler as Compiler>::Parser;
699    type Settings = <MultiCompiler as Compiler>::Settings;
700    type Language = <MultiCompiler as Compiler>::Language;
701
702    fn compile(
703        &self,
704        _input: &Self::Input,
705    ) -> std::result::Result<
706        CompilerOutput<Self::CompilationError, Self::CompilerContract>,
707        SolcError,
708    > {
709        Err(SolcError::msg("cached artifact selection cannot compile sources"))
710    }
711
712    fn available_versions(&self, language: &Self::Language) -> Vec<CompilerVersion> {
713        if language.is_solc() { self.solc.clone() } else { self.vyper.clone() }
714    }
715}