Fork of daniellemaywood.uk/gleam — Wasm codegen work
28 kB
771 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::{BeamCompilerIO, 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 + BeamCompilerIO + 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 Target::Cranelift => super::TargetCodegenConfiguration::Cranelift {},
623 };
624
625 let mut compiler = PackageCompiler::new(
626 config,
627 mode,
628 &root_path,
629 &out_path,
630 &lib_path,
631 &target,
632 self.ids.clone(),
633 self.io.clone(),
634 );
635 compiler.write_metadata = true;
636 compiler.write_entrypoint = is_root;
637 compiler.perform_codegen = self.options.codegen.should_codegen(is_root);
638 compiler.compile_beam_bytecode = self.options.codegen.should_codegen(is_root);
639 compiler.compile_modules = !(self.options.compile == Compile::DepsOnly && is_root);
640 compiler.subprocess_stdio = self.subprocess_stdio;
641 compiler.target_support = if is_root {
642 // When compiling the root package it is context specific as to whether we need to
643 // enforce that all functions have an implementation for the current target.
644 // Typically we do, but if we are using `gleam run -m $module` to run a module that
645 // belongs to a dependency we don't need to enforce this as we don't want to fail
646 // compilation. It's impossible for a dependecy module to call functions from the root
647 // package, so it's OK if they could not be compiled.
648 self.options.root_target_support
649 } else {
650 // When compiling dependencies we don't enforce that all functions have an
651 // implementation for the current target. It is OK if they have APIs that are
652 // unaccessible so long as they are not used by the root package.
653 TargetSupport::NotEnforced
654 };
655 if is_root {
656 compiler.cached_warnings = CachedWarnings::Use;
657 // We only check for conflicting Gleam files if this is the root
658 // package, since Hex packages are bundled with the Gleam source files
659 // and compiled Erlang files next to each other.
660 compiler.check_module_conflicts = CheckModuleConflicts::Check;
661 } else {
662 compiler.cached_warnings = CachedWarnings::Ignore;
663 compiler.check_module_conflicts = CheckModuleConflicts::DoNotCheck;
664 };
665
666 // Compile project to Erlang or JavaScript source code
667 compiler.compile(
668 &mut self.warnings,
669 &mut self.importable_modules,
670 &mut self.defined_modules,
671 &mut self.stale_modules,
672 &mut self.incomplete_modules,
673 self.telemetry,
674 )
675 }
676}
677
678impl<IO> ProjectCompiler<IO> {
679 pub fn get_importable_modules(&self) -> &im::HashMap<EcoString, type_::ModuleInterface> {
680 &self.importable_modules
681 }
682}
683
684fn order_packages(packages: &HashMap<String, ManifestPackage>) -> Result<Vec<EcoString>, Error> {
685 dep_tree::toposort_deps(
686 packages
687 .values()
688 // Making sure that the package order is deterministic, to prevent different
689 // compilations of the same project compiling in different orders. This could impact
690 // any bugged outcomes, though not any where the compiler is working correctly, so it's
691 // mostly to aid debugging.
692 .sorted_by(|a, b| a.name.cmp(&b.name))
693 .map(|package| {
694 (
695 package.name.as_str().into(),
696 package
697 .requirements
698 .iter()
699 .map(|r| EcoString::from(r.as_ref()))
700 .collect(),
701 )
702 })
703 .collect(),
704 )
705 .map_err(convert_deps_tree_error)
706}
707
708fn convert_deps_tree_error(e: dep_tree::Error) -> Error {
709 match e {
710 dep_tree::Error::Cycle(packages) => Error::PackageCycle { packages },
711 }
712}
713
714#[derive(Debug, PartialEq, Clone, Copy)]
715pub(crate) enum BuildTool {
716 Gleam,
717 Rebar3,
718 Mix,
719}
720
721/// Determine the build tool we should use to build this package
722pub(crate) fn usable_build_tools(package: &ManifestPackage) -> Result<Vec<BuildTool>, Error> {
723 let mut rebar3_present = false;
724 let mut mix_present = false;
725
726 for tool in &package.build_tools {
727 match tool.as_str() {
728 "gleam" => return Ok(vec![BuildTool::Gleam]),
729 "rebar" => rebar3_present = true,
730 "rebar3" => rebar3_present = true,
731 "mix" => mix_present = true,
732 _ => (),
733 }
734 }
735
736 if mix_present && rebar3_present {
737 return Ok(vec![BuildTool::Mix, BuildTool::Rebar3]);
738 } else if mix_present {
739 return Ok(vec![BuildTool::Mix]);
740 } else if rebar3_present {
741 return Ok(vec![BuildTool::Rebar3]);
742 }
743
744 Err(Error::UnsupportedBuildTool {
745 package: package.name.to_string(),
746 build_tools: package.build_tools.clone(),
747 })
748}
749
750#[derive(Debug)]
751struct BitFlags {
752 bits: u8,
753}
754
755impl BitFlags {
756 fn new() -> Self {
757 Self { bits: 0 }
758 }
759
760 fn set(&mut self, bit: u8, value: bool) {
761 if value {
762 self.bits |= 1 << bit;
763 } else {
764 self.bits &= !(1 << bit);
765 }
766 }
767
768 fn get(&self, bit: u8) -> bool {
769 self.bits & (1 << bit) != 0
770 }
771}