Fork of daniellemaywood.uk/gleam — Wasm codegen work
26 kB
731 lines
1use crate::{
2 Error, Result, Warning,
3 analyse::TargetSupport,
4 build::{
5 Mode, Module, Origin, Package, Target,
6 package_compiler::{self, PackageCompiler},
7 package_loader::StaleTracker,
8 project_compiler,
9 telemetry::Telemetry,
10 },
11 codegen::{self, ErlangApp},
12 config::PackageConfig,
13 dep_tree,
14 error::{DefinedModuleOrigin, FileIoAction, FileKind, ShellCommandFailureReason},
15 io::{BeamCompiler, Command, CommandExecutor, FileSystemReader, FileSystemWriter, Stdio},
16 manifest::{ManifestPackage, ManifestPackageSource},
17 metadata,
18 paths::{self, ProjectPaths},
19 type_::{self, ModuleFunction},
20 uid::UniqueIdGenerator,
21 version::COMPILER_VERSION,
22 warning::{self, WarningEmitter, WarningEmitterIO},
23};
24use ecow::EcoString;
25use hexpm::version::Version;
26use itertools::Itertools;
27use pubgrub::Range;
28use std::{
29 cmp,
30 collections::{HashMap, HashSet},
31 fmt::Write,
32 io::BufReader,
33 rc::Rc,
34 sync::Arc,
35 time::Instant,
36};
37
38use super::{
39 Codegen, Compile, ErlangAppCodegenConfiguration, Outcome,
40 elixir_libraries::ElixirLibraries,
41 package_compiler::{CachedWarnings, CheckModuleConflicts, Compiled},
42};
43
44use camino::{Utf8Path, Utf8PathBuf};
45
46// On Windows we have to call rebar3 via a little wrapper script.
47//
48#[cfg(not(target_os = "windows"))]
49const REBAR_EXECUTABLE: &str = "rebar3";
50#[cfg(target_os = "windows")]
51const REBAR_EXECUTABLE: &str = "rebar3.cmd";
52
53#[cfg(not(target_os = "windows"))]
54const ELIXIR_EXECUTABLE: &str = "elixir";
55#[cfg(target_os = "windows")]
56const ELIXIR_EXECUTABLE: &str = "elixir.bat";
57
58#[derive(Debug)]
59pub struct Options {
60 pub mode: Mode,
61 pub target: Option<Target>,
62 pub compile: Compile,
63 pub codegen: Codegen,
64 pub warnings_as_errors: bool,
65 pub root_target_support: TargetSupport,
66 pub no_print_progress: bool,
67}
68
69#[derive(Debug)]
70pub struct Built {
71 pub root_package: Package,
72 pub module_interfaces: im::HashMap<EcoString, type_::ModuleInterface>,
73 compiled_dependency_modules: Vec<Module>,
74}
75
76impl Built {
77 pub fn get_main_function(
78 &self,
79 module: &EcoString,
80 target: Target,
81 ) -> Result<ModuleFunction, Error> {
82 match self.module_interfaces.get(module) {
83 Some(module_data) => module_data.get_main_function(target),
84 None => Err(Error::ModuleDoesNotExist {
85 module: module.clone(),
86 suggestion: None,
87 }),
88 }
89 }
90
91 pub fn minimum_required_version(&self) -> Version {
92 self.module_interfaces
93 .values()
94 .map(|interface| &interface.minimum_required_version)
95 .reduce(|one_version, other_version| cmp::max(one_version, other_version))
96 .map(|minimum_required_version| minimum_required_version.clone())
97 .unwrap_or(Version::new(0, 1, 0))
98 }
99}
100
101#[derive(Debug)]
102pub struct ProjectCompiler<IO> {
103 // The gleam.toml config for the root package of the project
104 pub config: PackageConfig,
105 pub packages: HashMap<String, ManifestPackage>,
106 importable_modules: im::HashMap<EcoString, type_::ModuleInterface>,
107 pub(crate) defined_modules: im::HashMap<EcoString, DefinedModuleOrigin>,
108 stale_modules: StaleTracker,
109 /// The set of modules that have had partial compilation done since the last
110 /// successful compilation.
111 incomplete_modules: HashSet<EcoString>,
112 warnings: WarningEmitter,
113 telemetry: &'static dyn Telemetry,
114 options: Options,
115 paths: ProjectPaths,
116 ids: UniqueIdGenerator,
117 pub io: IO,
118 /// We may want to silence subprocess stdout if we are running in LSP mode.
119 /// The language server talks over stdio so printing would break that.
120 pub subprocess_stdio: Stdio,
121}
122
123// TODO: test that tests cannot be imported into src
124// TODO: test that dep cycles are not allowed between packages
125
126impl<IO> ProjectCompiler<IO>
127where
128 IO: CommandExecutor + FileSystemWriter + FileSystemReader + BeamCompiler + Clone,
129{
130 pub fn new(
131 config: PackageConfig,
132 options: Options,
133 packages: Vec<ManifestPackage>,
134 telemetry: &'static dyn Telemetry,
135 warning_emitter: Rc<dyn WarningEmitterIO>,
136 paths: ProjectPaths,
137 io: IO,
138 ) -> Self {
139 let packages = packages
140 .into_iter()
141 .map(|p| (p.name.to_string(), p))
142 .collect();
143
144 Self {
145 importable_modules: im::HashMap::new(),
146 defined_modules: im::HashMap::new(),
147 stale_modules: StaleTracker::default(),
148 incomplete_modules: HashSet::new(),
149 ids: UniqueIdGenerator::new(),
150 warnings: WarningEmitter::new(warning_emitter),
151 subprocess_stdio: Stdio::Inherit,
152 telemetry,
153 packages,
154 options,
155 config,
156 paths,
157 io,
158 }
159 }
160
161 pub fn mode(&self) -> Mode {
162 self.options.mode
163 }
164
165 pub fn target(&self) -> Target {
166 self.options.target.unwrap_or(self.config.target)
167 }
168
169 pub fn reset_state_for_new_compile_run(&mut self) {
170 // We make sure the stale module tracker is empty before we start, to
171 // avoid mistakenly thinking a module is stale due to outdated state
172 // from a previous build. A ProjectCompiler instance is re-used by the
173 // LSP engine so state could be reused if we don't reset it.
174
175 self.stale_modules.empty();
176
177 /// We also clear the defined modules, otherwise the language server
178 /// would start throwing errors for modules defined twice when compiling
179 /// a second time!
180 self.defined_modules.clear();
181 }
182
183 fn retain_only_production_packages(&mut self) {
184 let mut production = HashSet::new();
185 let mut queue: Vec<_> = self.config.dependencies.keys().collect();
186 while let Some(name) = queue.pop() {
187 if production.insert(name.clone())
188 && let Some(pkg) = self.packages.get(name.as_str())
189 {
190 queue.extend(pkg.requirements.iter());
191 }
192 }
193 self.packages
194 .retain(|name, _| production.contains(name.as_str()));
195 }
196
197 /// Compiles all packages in the project and returns the compiled
198 /// information from the root package
199 pub fn compile(mut self) -> Result<Built> {
200 self.reset_state_for_new_compile_run();
201
202 // In production mode, skip dev-only dependencies entirely so they
203 // are never compiled.
204 if self.mode() == Mode::Prod {
205 self.retain_only_production_packages();
206 }
207
208 // Each package may specify a Gleam version that it supports, so we
209 // verify that this version is appropriate.
210 self.check_gleam_version()?;
211
212 // The JavaScript target requires a prelude module to be written.
213 self.write_prelude()?;
214
215 // Dependencies are compiled first.
216 let compiled_dependency_modules = self.compile_dependencies()?;
217
218 // We reset the warning count as we don't want to fail the build if a
219 // dependency has warnings, only if the root package does.
220 self.warnings.reset_count();
221
222 let root_package = self.compile_root_package().into_result()?;
223
224 // TODO: test
225 if self.options.warnings_as_errors && self.warnings.count() > 0 {
226 return Err(Error::ForbiddenWarnings {
227 count: self.warnings.count(),
228 });
229 }
230
231 Ok(Built {
232 root_package,
233 module_interfaces: self.importable_modules,
234 compiled_dependency_modules,
235 })
236 }
237
238 pub fn compile_root_package(&mut self) -> Outcome<Package, Error> {
239 let config = self.config.clone();
240 self.compile_gleam_package(&config, true, self.paths.root().to_path_buf())
241 .map(
242 |Compiled {
243 modules,
244 cached_module_names,
245 }| Package {
246 config,
247 modules,
248 cached_module_names,
249 },
250 )
251 }
252
253 /// Checks that version file found in the build directory matches the
254 /// current version of gleam. If not, we will clear the build directory
255 /// before continuing. This will ensure that upgrading gleam will not leave
256 /// one with confusing or hard to debug states.
257 pub fn check_gleam_version(&self) -> Result<(), Error> {
258 let build_path = self
259 .paths
260 .build_directory_for_target(self.mode(), self.target());
261 let version_path = self.paths.build_gleam_version(self.mode(), self.target());
262 if self.io.is_file(&version_path) {
263 let version = self.io.read(&version_path)?;
264 if version == COMPILER_VERSION {
265 return Ok(());
266 }
267 }
268
269 // Either file is missing our the versions do not match. Time to rebuild
270 tracing::info!("removing_build_state_from_different_gleam_version");
271 self.io.delete_directory(&build_path)?;
272
273 // Recreate build directory with new updated version file
274 self.io.mkdir(&build_path)?;
275 self.io
276 .write(&version_path, COMPILER_VERSION)
277 .map_err(|e| Error::FileIo {
278 action: FileIoAction::WriteTo,
279 kind: FileKind::File,
280 path: version_path,
281 err: Some(e.to_string()),
282 })
283 }
284
285 pub fn compile_dependencies(&mut self) -> Result<Vec<Module>, Error> {
286 assert!(
287 self.stale_modules.is_empty(),
288 "The project compiler stale tracker was not emptied from the previous compilation"
289 );
290
291 let sequence = order_packages(&self.packages)?;
292 let mut modules = vec![];
293
294 for name in sequence {
295 let compiled = self.load_cache_or_compile_package(&name)?;
296 modules.extend(compiled);
297 }
298
299 Ok(modules)
300 }
301
302 fn write_prelude(&self) -> Result<()> {
303 // Only the JavaScript target has a prelude to write.
304 if !self.target().is_javascript() {
305 return Ok(());
306 }
307
308 let build = self
309 .paths
310 .build_directory_for_target(self.mode(), self.target());
311
312 // Write the JavaScript prelude
313 let path = build.join("prelude.mjs");
314 if !self.io.is_file(&path) {
315 self.io.write(&path, crate::javascript::PRELUDE)?;
316 }
317
318 // Write the TypeScript prelude, if asked for
319 if self.config.javascript.typescript_declarations {
320 let path = build.join("prelude.d.mts");
321 if !self.io.is_file(&path) {
322 self.io.write(&path, crate::javascript::PRELUDE_TS_DEF)?;
323 }
324 }
325
326 Ok(())
327 }
328
329 fn load_cache_or_compile_package(&mut self, name: &str) -> Result<Vec<Module>, Error> {
330 // TODO: We could remove this clone if we split out the compilation of
331 // packages into their own classes and then only mutate self after we no
332 // longer need to have the package borrowed from self.packages.
333 let package = self.packages.get(name).expect("Missing package").clone();
334 let result = match usable_build_tools(&package)?.as_slice() {
335 &[BuildTool::Gleam] => self.compile_gleam_dep_package(&package),
336 &[BuildTool::Rebar3] => self.compile_rebar3_dep_package(&package).map(|_| vec![]),
337 &[BuildTool::Mix] => self.compile_mix_dep_package(&package).map(|_| vec![]),
338 &[BuildTool::Mix, BuildTool::Rebar3] => self
339 .compile_mix_dep_package(&package)
340 .or_else(|_| self.compile_rebar3_dep_package(&package))
341 .map(|_| vec![]),
342 _ => {
343 return Err(Error::UnsupportedBuildTool {
344 package: package.name.to_string(),
345 build_tools: package.build_tools.clone(),
346 });
347 }
348 };
349
350 // TODO: test. This one is not covered by the integration tests.
351 if result.is_err() {
352 tracing::debug!(package=%name, "removing_failed_build");
353 let path = self.paths.build_directory_for_package(
354 self.mode(),
355 self.target(),
356 package.application_name(),
357 );
358 self.io.delete_directory(&path)?;
359 }
360
361 result
362 }
363
364 // TODO: extract and unit test
365 fn compile_rebar3_dep_package(&mut self, package: &ManifestPackage) -> Result<(), Error> {
366 let application_name = package.application_name();
367 let package_name = &package.name;
368 let mode = self.mode();
369 let target = self.target();
370
371 let package_build = self
372 .paths
373 .build_directory_for_package(mode, target, application_name);
374
375 // TODO: test
376 if self.io.is_directory(&package_build) {
377 tracing::debug!(%package_name, "using_precompiled_rebar3_package");
378 return Ok(());
379 }
380
381 // TODO: test
382 if !self.options.codegen.should_codegen(false) {
383 tracing::debug!(%package_name, "skipping_rebar3_build_as_codegen_disabled");
384 return Ok(());
385 }
386
387 // TODO: test
388 if target != Target::Erlang {
389 tracing::debug!(%package_name, "skipping_rebar3_build_for_non_erlang_target");
390 return Ok(());
391 }
392
393 // Print that work is being done
394 self.telemetry.compiling_package(package_name);
395
396 let package = self.paths.build_packages_package(package_name);
397 let build_packages = self.paths.build_directory_for_target(mode, target);
398 let ebins = self.paths.build_packages_ebins_glob(mode, target);
399 let rebar3_path = |path: &Utf8Path| format!("../{}", path);
400
401 tracing::debug!("copying_package_to_build");
402 self.io.mkdir(&package_build)?;
403 self.io.copy_dir(&package, &package_build)?;
404
405 let env = vec![
406 ("ERL_LIBS".to_string(), "../*/ebin".to_string()),
407 (
408 "REBAR_BARE_COMPILER_OUTPUT_DIR".to_string(),
409 package_build.to_string(),
410 ),
411 ("REBAR_PROFILE".to_string(), "prod".to_string()),
412 ("REBAR_SKIP_PROJECT_PLUGINS".to_string(), "true".to_string()),
413 ("TERM".to_string(), "dumb".to_string()),
414 ];
415 let args = vec![
416 "bare".into(),
417 "compile".into(),
418 "--paths".into(),
419 "../*/ebin".into(),
420 ];
421
422 let status = self.io.exec(Command {
423 program: REBAR_EXECUTABLE.into(),
424 args,
425 env,
426 cwd: Some(package_build),
427 stdio: self.subprocess_stdio,
428 })?;
429
430 if status == 0 {
431 Ok(())
432 } else {
433 Err(Error::ShellCommand {
434 program: "rebar3".into(),
435 reason: ShellCommandFailureReason::Unknown,
436 })
437 }
438 }
439
440 fn compile_mix_dep_package(&mut self, package: &ManifestPackage) -> Result<(), Error> {
441 let application_name = package.application_name();
442 let package_name = &package.name;
443 let mode = self.mode();
444 let target = self.target();
445 let mix_target = "prod";
446
447 let dest = self
448 .paths
449 .build_directory_for_package(mode, target, application_name);
450
451 // TODO: test
452 if self.io.is_directory(&dest) {
453 tracing::debug!(%package_name, "using_precompiled_mix_package");
454 return Ok(());
455 }
456
457 // TODO: test
458 if !self.options.codegen.should_codegen(false) {
459 tracing::debug!(%package_name, "skipping_mix_build_as_codegen_disabled");
460 return Ok(());
461 }
462
463 // TODO: test
464 if target != Target::Erlang {
465 tracing::debug!(%package_name, "skipping_mix_build_for_non_erlang_target");
466 return Ok(());
467 }
468
469 // Print that work is being done
470 self.telemetry.compiling_package(package_name);
471
472 let build_dir = self.paths.build_directory_for_target(mode, target);
473 let project_dir = self.paths.build_packages_package(package_name);
474 let mix_build_dir = project_dir.join("_build").join(mix_target);
475 let mix_build_lib_dir = mix_build_dir.join("lib");
476 let up = paths::unnest(&project_dir);
477 let mix_path = |path: &Utf8Path| up.join(path).to_string();
478 let ebins = self.paths.build_packages_ebins_glob(mode, target);
479
480 // Elixir core libs must be loaded
481 ElixirLibraries::make_available(&self.io, &build_dir, self.subprocess_stdio)?;
482
483 // Prevent Mix.Compilers.ApplicationTracer warnings
484 // mix would make this if it didn't exist, but we make it anyway as
485 // we need to link the compiled dependencies into there
486 self.io.mkdir(&mix_build_lib_dir)?;
487 let deps = &package.requirements;
488 for dep in deps {
489 // TODO: unit test
490 let dep_source = build_dir.join(dep.as_str());
491 let dep_dest = mix_build_lib_dir.join(dep.as_str());
492 if self.io.is_directory(&dep_source) && !self.io.is_directory(&dep_dest) {
493 tracing::debug!("linking_{}_to_build", dep);
494 self.io.symlink_dir(&dep_source, &dep_dest)?;
495 }
496 }
497
498 let env = vec![
499 ("MIX_BUILD_PATH".to_string(), mix_path(&mix_build_dir)),
500 ("MIX_ENV".to_string(), mix_target.to_string()),
501 ("MIX_QUIET".to_string(), "1".to_string()),
502 ("TERM".to_string(), "dumb".to_string()),
503 ];
504 let args = vec![
505 "-pa".to_string(),
506 mix_path(&ebins),
507 "-S".to_string(),
508 "mix".to_string(),
509 "compile".to_string(),
510 "--no-deps-check".to_string(),
511 "--no-load-deps".to_string(),
512 "--no-protocol-consolidation".to_string(),
513 ];
514
515 let status = self.io.exec(Command {
516 program: ELIXIR_EXECUTABLE.into(),
517 args,
518 env,
519 cwd: Some(project_dir),
520 stdio: self.subprocess_stdio,
521 })?;
522
523 if status == 0 {
524 // TODO: unit test
525 let source = mix_build_dir.join("lib").join(application_name.as_str());
526 if self.io.is_directory(&source) && !self.io.is_directory(&dest) {
527 tracing::debug!("linking_{}_to_build", application_name);
528 self.io.symlink_dir(&source, &dest)?;
529 }
530 Ok(())
531 } else {
532 Err(Error::ShellCommand {
533 program: "mix".into(),
534 reason: ShellCommandFailureReason::Unknown,
535 })
536 }
537 }
538
539 fn compile_gleam_dep_package(
540 &mut self,
541 package: &ManifestPackage,
542 ) -> Result<Vec<Module>, Error> {
543 // TODO: Test
544 let package_root = match &package.source {
545 // If the path is relative it is relative to the root of the
546 // project, not to the current working directory. The language server
547 // could have the working directory and the project root in different
548 // places.
549 ManifestPackageSource::Local { path } if path.is_relative() => {
550 self.io.canonicalise(&self.paths.root().join(path))?
551 }
552
553 // If the path is absolute we can use it as-is.
554 ManifestPackageSource::Local { path } => path.clone(),
555
556 // Hex and Git packages are downloaded into the project's build
557 // directory.
558 ManifestPackageSource::Git { .. } | ManifestPackageSource::Hex { .. } => {
559 self.paths.build_packages_package(&package.name)
560 }
561 };
562 let config_path = package_root.join("gleam.toml");
563 let config = PackageConfig::read(config_path, &self.io)?;
564 self.compile_gleam_package(&config, false, package_root)
565 .into_result()
566 .map(|compiled| compiled.modules)
567 }
568
569 fn compile_gleam_package(
570 &mut self,
571 config: &PackageConfig,
572 is_root: bool,
573 root_path: Utf8PathBuf,
574 ) -> Outcome<Compiled, Error> {
575 let out_path =
576 self.paths
577 .build_directory_for_package(self.mode(), self.target(), &config.name);
578 let lib_path = self
579 .paths
580 .build_directory_for_target(self.mode(), self.target());
581 let mode = if is_root { self.mode() } else { Mode::Prod };
582 let target = match self.target() {
583 Target::Erlang => {
584 let package_name_overrides = self
585 .packages
586 .values()
587 .flat_map(|p| {
588 let overriden = p.otp_app.as_ref()?;
589 Some((p.name.clone(), overriden.clone()))
590 })
591 .collect();
592 super::TargetCodegenConfiguration::Erlang {
593 app_file: Some(ErlangAppCodegenConfiguration {
594 include_dev_deps: is_root && self.mode().includes_dev_dependencies(),
595 package_name_overrides,
596 }),
597 }
598 }
599
600 Target::JavaScript => super::TargetCodegenConfiguration::JavaScript {
601 emit_typescript_definitions: self.config.javascript.typescript_declarations,
602 emit_source_maps: self.config.javascript.source_maps,
603 // This path is relative to each package output directory
604 prelude_location: Utf8PathBuf::from("../prelude.mjs"),
605 },
606 };
607
608 let mut compiler = PackageCompiler::new(
609 config,
610 mode,
611 &root_path,
612 &out_path,
613 &lib_path,
614 &target,
615 self.ids.clone(),
616 self.io.clone(),
617 );
618 compiler.write_metadata = true;
619 compiler.write_entrypoint = is_root;
620 compiler.perform_codegen = self.options.codegen.should_codegen(is_root);
621 compiler.compile_beam_bytecode = self.options.codegen.should_codegen(is_root);
622 compiler.compile_modules = !(self.options.compile == Compile::DepsOnly && is_root);
623 compiler.subprocess_stdio = self.subprocess_stdio;
624 compiler.target_support = if is_root {
625 // When compiling the root package it is context specific as to whether we need to
626 // enforce that all functions have an implementation for the current target.
627 // Typically we do, but if we are using `gleam run -m $module` to run a module that
628 // belongs to a dependency we don't need to enforce this as we don't want to fail
629 // compilation. It's impossible for a dependecy module to call functions from the root
630 // package, so it's OK if they could not be compiled.
631 self.options.root_target_support
632 } else {
633 // When compiling dependencies we don't enforce that all functions have an
634 // implementation for the current target. It is OK if they have APIs that are
635 // unaccessible so long as they are not used by the root package.
636 TargetSupport::NotEnforced
637 };
638 if is_root {
639 compiler.cached_warnings = CachedWarnings::Use;
640 // We only check for conflicting Gleam files if this is the root
641 // package, since Hex packages are bundled with the Gleam source files
642 // and compiled Erlang files next to each other.
643 compiler.check_module_conflicts = CheckModuleConflicts::Check;
644 } else {
645 compiler.cached_warnings = CachedWarnings::Ignore;
646 compiler.check_module_conflicts = CheckModuleConflicts::DoNotCheck;
647 };
648
649 // Compile project to Erlang or JavaScript source code
650 compiler.compile(
651 &mut self.warnings,
652 &mut self.importable_modules,
653 &mut self.defined_modules,
654 &mut self.stale_modules,
655 &mut self.incomplete_modules,
656 self.telemetry,
657 )
658 }
659}
660
661impl<IO> ProjectCompiler<IO> {
662 pub fn get_importable_modules(&self) -> &im::HashMap<EcoString, type_::ModuleInterface> {
663 &self.importable_modules
664 }
665}
666
667fn order_packages(packages: &HashMap<String, ManifestPackage>) -> Result<Vec<EcoString>, Error> {
668 dep_tree::toposort_deps(
669 packages
670 .values()
671 // Making sure that the package order is deterministic, to prevent different
672 // compilations of the same project compiling in different orders. This could impact
673 // any bugged outcomes, though not any where the compiler is working correctly, so it's
674 // mostly to aid debugging.
675 .sorted_by(|a, b| a.name.cmp(&b.name))
676 .map(|package| {
677 (
678 package.name.as_str().into(),
679 package
680 .requirements
681 .iter()
682 .map(|r| EcoString::from(r.as_ref()))
683 .collect(),
684 )
685 })
686 .collect(),
687 )
688 .map_err(convert_deps_tree_error)
689}
690
691fn convert_deps_tree_error(e: dep_tree::Error) -> Error {
692 match e {
693 dep_tree::Error::Cycle(packages) => Error::PackageCycle { packages },
694 }
695}
696
697#[derive(Debug, PartialEq, Clone, Copy)]
698pub(crate) enum BuildTool {
699 Gleam,
700 Rebar3,
701 Mix,
702}
703
704/// Determine the build tool we should use to build this package
705pub(crate) fn usable_build_tools(package: &ManifestPackage) -> Result<Vec<BuildTool>, Error> {
706 let mut rebar3_present = false;
707 let mut mix_present = false;
708
709 for tool in &package.build_tools {
710 match tool.as_str() {
711 "gleam" => return Ok(vec![BuildTool::Gleam]),
712 "rebar" => rebar3_present = true,
713 "rebar3" => rebar3_present = true,
714 "mix" => mix_present = true,
715 _ => (),
716 }
717 }
718
719 if mix_present && rebar3_present {
720 return Ok(vec![BuildTool::Mix, BuildTool::Rebar3]);
721 } else if mix_present {
722 return Ok(vec![BuildTool::Mix]);
723 } else if rebar3_present {
724 return Ok(vec![BuildTool::Rebar3]);
725 }
726
727 Err(Error::UnsupportedBuildTool {
728 package: package.name.to_string(),
729 build_tools: package.build_tools.clone(),
730 })
731}