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