Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / build / project_compiler.rs
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}