1use super::program::{
2 ParsedModule, compile_parsed_host_package_program, compile_parsed_package_program, parse_module,
3};
4use super::{FrontendError, HostedTypedProgram, ModuleSource, TypedProgram};
5use crate::host::{HostProfile, HostProviderSet};
6use camino::{Utf8Path, Utf8PathBuf};
7use ecow::EcoString;
8use gleam_compiler_core::build::Target;
9use gleam_compiler_core::config::PackageConfig;
10use gleam_compiler_core::manifest::{Manifest, ManifestPackage, ManifestPackageSource};
11use gleam_compiler_core::type_::PRELUDE_MODULE_NAME;
12use gleam_compiler_core::warning::WarningEmitter;
13use std::collections::{BTreeMap, BTreeSet};
14use std::ffi::OsString;
15use std::fs;
16use thiserror::Error;
17
18const CONFIG_FILE: &str = "gleam.toml";
19const MANIFEST_FILE: &str = "manifest.toml";
20
21#[derive(Debug, Error)]
22pub enum ProjectError {
23 #[error("failed to read Gleam package config {path}")]
24 ConfigIo {
25 path: Utf8PathBuf,
26 #[source]
27 error: std::io::Error,
28 },
29
30 #[error("failed to read Gleam package manifest {path}")]
31 ManifestIo {
32 path: Utf8PathBuf,
33 #[source]
34 error: std::io::Error,
35 },
36
37 #[error("failed to read Gleam source {path}")]
38 SourceIo {
39 path: Utf8PathBuf,
40 #[source]
41 error: std::io::Error,
42 },
43
44 #[error("invalid Gleam package config {path}: {reason}")]
45 InvalidConfig { path: Utf8PathBuf, reason: String },
46
47 #[error("invalid Gleam package manifest {path}: {reason}")]
48 InvalidManifest { path: Utf8PathBuf, reason: String },
49
50 #[error("package {package} is incompatible with the embedded Gleam compiler: {error}")]
51 IncompatibleCompilerVersion {
52 package: EcoString,
53 error: Box<gleam_compiler_core::Error>,
54 },
55
56 #[error("package {required_by} requires {package}, but it is absent from manifest.toml")]
57 MissingManifestPackage {
58 required_by: EcoString,
59 package: EcoString,
60 },
61
62 #[error(
63 "resolved package {package} is not downloaded at {path}; run `gleam deps download` first"
64 )]
65 MissingDownloadedPackage {
66 package: EcoString,
67 path: Utf8PathBuf,
68 },
69
70 #[error(
71 "manifest package {expected} resolves to a package config named {actual} at {config_path}"
72 )]
73 PackageNameMismatch {
74 expected: EcoString,
75 actual: EcoString,
76 config_path: Utf8PathBuf,
77 },
78
79 #[error(transparent)]
80 Frontend(#[from] FrontendError),
81}
82
83pub fn compile_typed_project(
84 project_root: impl Into<Utf8PathBuf>,
85 root_module: impl Into<EcoString>,
86) -> Result<TypedProgram, ProjectError> {
87 let project = load_project(project_root.into(), root_module.into())?;
88 compile_parsed_package_program(
89 project.root_package,
90 project.root_module,
91 project.modules,
92 WarningEmitter::null(),
93 )
94 .map_err(ProjectError::from)
95}
96
97pub fn compile_typed_host_project<Profile: HostProfile>(
103 project_root: impl Into<Utf8PathBuf>,
104 root_module: impl Into<EcoString>,
105 hosts: HostProviderSet<Profile>,
106) -> Result<HostedTypedProgram<Profile>, ProjectError> {
107 let project = load_project(project_root.into(), root_module.into())?;
108 let selected_source_modules = project
109 .modules
110 .iter()
111 .map(|module| (module.package.clone(), module.module.name.clone()))
112 .collect::<BTreeSet<_>>();
113 let hosts = hosts.select_source_providers(&selected_source_modules);
114 compile_parsed_host_package_program(
115 project.root_package,
116 project.root_module,
117 project.modules,
118 hosts,
119 WarningEmitter::null(),
120 )
121 .map_err(ProjectError::from)
122}
123
124struct ParsedProject {
125 root_package: EcoString,
126 root_module: EcoString,
127 modules: Vec<ParsedModule>,
128}
129
130fn load_project(
131 project_root: Utf8PathBuf,
132 root_module: EcoString,
133) -> Result<ParsedProject, ProjectError> {
134 let root_config = read_config(&project_root.join(CONFIG_FILE))?;
135 let manifest = read_manifest(&project_root.join(MANIFEST_FILE))?;
136 let manifest_packages = manifest_packages(&project_root, &manifest)?;
137 let packages = load_packages(&project_root, root_config, &manifest_packages)?;
138 let catalog = source_catalog(&packages)?;
139 let modules = select_import_closure(&packages.root, &root_module, &catalog)?;
140
141 Ok(ParsedProject {
142 root_package: packages.root.name,
143 root_module,
144 modules,
145 })
146}
147
148fn read_config(path: &Utf8Path) -> Result<PackageConfig, ProjectError> {
149 let source = fs::read_to_string(path).map_err(|error| ProjectError::ConfigIo {
150 path: path.to_path_buf(),
151 error,
152 })?;
153 let config =
154 toml::from_str::<PackageConfig>(&source).map_err(|error| ProjectError::InvalidConfig {
155 path: path.to_path_buf(),
156 reason: error.to_string(),
157 })?;
158 config.check_gleam_compatibility().map_err(|error| {
159 ProjectError::IncompatibleCompilerVersion {
160 package: config.name.clone(),
161 error: Box::new(error),
162 }
163 })?;
164 Ok(config)
165}
166
167fn read_manifest(path: &Utf8Path) -> Result<Manifest, ProjectError> {
168 let source = fs::read_to_string(path).map_err(|error| ProjectError::ManifestIo {
169 path: path.to_path_buf(),
170 error,
171 })?;
172 toml::from_str(&source).map_err(|error| ProjectError::InvalidManifest {
173 path: path.to_path_buf(),
174 reason: error.to_string(),
175 })
176}
177
178fn manifest_packages<'manifest>(
179 project_root: &Utf8Path,
180 manifest: &'manifest Manifest,
181) -> Result<BTreeMap<EcoString, &'manifest ManifestPackage>, ProjectError> {
182 let mut packages = BTreeMap::new();
183 for package in &manifest.packages {
184 if packages.insert(package.name.clone(), package).is_some() {
185 return Err(ProjectError::InvalidManifest {
186 path: project_root.join(MANIFEST_FILE),
187 reason: format!("package {} is listed more than once", package.name),
188 });
189 }
190 }
191 Ok(packages)
192}
193
194struct LoadedPackage {
195 name: EcoString,
196 root: Utf8PathBuf,
197 direct_dependencies: Box<[EcoString]>,
198}
199
200struct LoadedProject {
201 root: LoadedPackage,
202 dependencies: Vec<LoadedPackage>,
203}
204
205fn load_packages(
206 project_root: &Utf8Path,
207 root_config: PackageConfig,
208 manifest_packages: &BTreeMap<EcoString, &ManifestPackage>,
209) -> Result<LoadedProject, ProjectError> {
210 let root_name = root_config.name.clone();
211 let root_dependencies = dependency_names(&root_config);
212 let root = LoadedPackage {
213 name: root_name.clone(),
214 root: project_root.to_path_buf(),
215 direct_dependencies: root_dependencies.clone(),
216 };
217 let mut dependencies = Vec::new();
218 let mut loaded = BTreeSet::from([root_name.clone()]);
219 let mut pending = root_dependencies
220 .iter()
221 .cloned()
222 .map(|package| (root_name.clone(), package))
223 .collect::<BTreeSet<_>>();
224
225 while let Some((required_by, package_name)) = pending.pop_first() {
226 if loaded.contains(&package_name) {
227 continue;
228 }
229 let package = manifest_packages
230 .get(&package_name)
231 .copied()
232 .ok_or_else(|| ProjectError::MissingManifestPackage {
233 required_by,
234 package: package_name.clone(),
235 })?;
236 loaded.insert(package_name.clone());
237 if !package.build_tools.iter().any(|tool| tool == "gleam") {
238 continue;
239 }
240
241 let package_root = package_root(project_root, package);
242 if !package_root.is_dir() && !package.is_local() {
243 return Err(ProjectError::MissingDownloadedPackage {
244 package: package_name,
245 path: package_root,
246 });
247 }
248 let config_path = package_root.join(CONFIG_FILE);
249 let config = read_config(&config_path)?;
250 if config.name != package.name {
251 return Err(ProjectError::PackageNameMismatch {
252 expected: package.name.clone(),
253 actual: config.name,
254 config_path,
255 });
256 }
257 let direct_dependencies = dependency_names(&config);
258 for dependency in &direct_dependencies {
259 pending.insert((package.name.clone(), dependency.clone()));
260 }
261 dependencies.push(LoadedPackage {
262 name: package.name.clone(),
263 root: package_root,
264 direct_dependencies,
265 });
266 }
267
268 Ok(LoadedProject { root, dependencies })
269}
270
271fn dependency_names(config: &PackageConfig) -> Box<[EcoString]> {
272 config
273 .dependencies
274 .keys()
275 .cloned()
276 .collect::<BTreeSet<_>>()
277 .into_iter()
278 .collect()
279}
280
281fn package_root(project_root: &Utf8Path, package: &ManifestPackage) -> Utf8PathBuf {
282 match &package.source {
283 ManifestPackageSource::Hex { .. } | ManifestPackageSource::Git { .. } => project_root
284 .join("build")
285 .join("packages")
286 .join(package.name.as_str()),
287 ManifestPackageSource::Local { path } if path.is_absolute() => path.clone(),
288 ManifestPackageSource::Local { path } => project_root.join(path),
289 }
290}
291
292#[derive(Clone)]
293struct CatalogModule {
294 package: EcoString,
295 direct_dependencies: Box<[EcoString]>,
296 module: EcoString,
297 path: Utf8PathBuf,
298}
299
300fn source_catalog(
301 project: &LoadedProject,
302) -> Result<BTreeMap<EcoString, Vec<CatalogModule>>, ProjectError> {
303 let mut catalog = BTreeMap::<EcoString, Vec<CatalogModule>>::new();
304 for package in std::iter::once(&project.root).chain(&project.dependencies) {
305 let source_root = package.root.join("src");
306 for (module, path) in source_paths(&source_root)? {
307 catalog
308 .entry(module.clone())
309 .or_default()
310 .push(CatalogModule {
311 package: package.name.clone(),
312 direct_dependencies: package.direct_dependencies.clone(),
313 module,
314 path,
315 });
316 }
317 }
318 Ok(catalog)
319}
320
321fn source_paths(directory: &Utf8Path) -> Result<Vec<(EcoString, Utf8PathBuf)>, ProjectError> {
322 source_paths_from(&FileSystemSourceDirectory, directory, Utf8Path::new(""))
323}
324
325trait SourceDirectory {
326 fn entries(&self, directory: &Utf8Path)
327 -> Result<Vec<std::io::Result<OsString>>, ProjectError>;
328}
329
330struct FileSystemSourceDirectory;
331
332impl SourceDirectory for FileSystemSourceDirectory {
333 fn entries(
334 &self,
335 directory: &Utf8Path,
336 ) -> Result<Vec<std::io::Result<OsString>>, ProjectError> {
337 fs::read_dir(directory)
338 .map(|entries| {
339 entries
340 .map(|entry| entry.map(|entry| entry.file_name()))
341 .collect()
342 })
343 .map_err(|error| ProjectError::SourceIo {
344 path: directory.to_path_buf(),
345 error,
346 })
347 }
348}
349
350fn source_paths_from(
351 source_directory: &dyn SourceDirectory,
352 directory: &Utf8Path,
353 relative_directory: &Utf8Path,
354) -> Result<Vec<(EcoString, Utf8PathBuf)>, ProjectError> {
355 let mut paths = Vec::new();
356 for entry in source_directory.entries(directory)? {
357 let file_name = source_file_name(
358 directory,
359 entry.map_err(|error| ProjectError::SourceIo {
360 path: directory.to_path_buf(),
361 error,
362 })?,
363 )?;
364 let path = directory.join(&file_name);
365 let relative = relative_directory.join(file_name);
366 if path.is_dir() {
367 paths.extend(source_paths_from(source_directory, &path, &relative)?);
368 } else if path.extension() == Some("gleam") {
369 let module = relative
370 .with_extension("")
371 .as_str()
372 .replace('\\', "/")
373 .into();
374 paths.push((module, path));
375 }
376 }
377 paths.sort_by(|left, right| left.1.cmp(&right.1));
378 Ok(paths)
379}
380
381fn source_file_name(directory: &Utf8Path, file_name: OsString) -> Result<String, ProjectError> {
382 file_name
383 .into_string()
384 .map_err(|file_name| ProjectError::SourceIo {
385 path: directory.to_path_buf(),
386 error: std::io::Error::new(
387 std::io::ErrorKind::InvalidData,
388 format!("non-UTF-8 source path: {}", file_name.to_string_lossy()),
389 ),
390 })
391}
392
393fn select_import_closure(
394 root_package: &LoadedPackage,
395 root_module: &EcoString,
396 catalog: &BTreeMap<EcoString, Vec<CatalogModule>>,
397) -> Result<Vec<ParsedModule>, ProjectError> {
398 let root = catalog
399 .get(root_module)
400 .and_then(|modules| {
401 modules
402 .iter()
403 .find(|module| module.package == root_package.name)
404 })
405 .cloned()
406 .ok_or_else(|| {
407 ProjectError::Frontend(FrontendError::MissingRootModule {
408 package: root_package.name.clone(),
409 module: root_module.clone(),
410 })
411 })?;
412 let warnings = WarningEmitter::null();
413 let mut pending = BTreeMap::from([((root.package.clone(), root.module.clone()), root)]);
414 let mut selected = BTreeSet::new();
415 let mut parsed_modules = Vec::new();
416
417 while let Some((identity, module)) = pending.pop_first() {
418 if !selected.insert(identity) {
419 continue;
420 }
421 let source = fs::read_to_string(&module.path).map_err(|error| ProjectError::SourceIo {
422 path: module.path.clone(),
423 error,
424 })?;
425 let parsed = parse_module(
426 module.package,
427 module.direct_dependencies,
428 ModuleSource::new(module.module, module.path, source),
429 &warnings,
430 )?;
431 for (dependency, _) in parsed.module.dependencies(Target::Erlang) {
432 if dependency == PRELUDE_MODULE_NAME {
433 continue;
434 }
435 if let Some(modules) = catalog.get(&dependency) {
436 for module in modules {
437 pending.insert(
438 (module.package.clone(), module.module.clone()),
439 module.clone(),
440 );
441 }
442 }
443 }
444 parsed_modules.push(parsed);
445 }
446
447 Ok(parsed_modules)
448}
449
450#[cfg(test)]
451mod tests {
452 use super::{
453 ProjectError, SourceDirectory, compile_typed_host_project, compile_typed_project,
454 source_paths_from,
455 };
456 use crate::host::{HostModule, HostProviderModule, HostProviderSet, StatelessHostProfile};
457 use crate::planner::UnsupportedFunctionReason;
458 use crate::{HostedExecution, PlanError, Value, plan_host_program, plan_program};
459 use camino::{Utf8Path, Utf8PathBuf};
460 use num_bigint::BigInt;
461 use std::fs;
462 use tempfile::{TempDir, tempdir};
463
464 struct FailedSourceDirectoryEntry;
465
466 impl SourceDirectory for FailedSourceDirectoryEntry {
467 fn entries(
468 &self,
469 _directory: &Utf8Path,
470 ) -> Result<Vec<std::io::Result<std::ffi::OsString>>, ProjectError> {
471 Ok(vec![Err(std::io::Error::new(
472 std::io::ErrorKind::PermissionDenied,
473 "entry unavailable",
474 ))])
475 }
476 }
477
478 #[cfg(unix)]
479 struct NonUtf8SourceDirectoryEntry;
480
481 #[cfg(unix)]
482 impl SourceDirectory for NonUtf8SourceDirectoryEntry {
483 fn entries(
484 &self,
485 _directory: &Utf8Path,
486 ) -> Result<Vec<std::io::Result<std::ffi::OsString>>, ProjectError> {
487 use std::os::unix::ffi::OsStringExt;
488
489 Ok(vec![Ok(std::ffi::OsString::from_vec(
490 b"invalid-\xff.gleam".to_vec(),
491 ))])
492 }
493 }
494
495 #[test]
496 fn loads_selected_hex_git_and_local_package_modules() {
497 let project = tempdir().expect("temporary project should be created");
498 let root = project_root(&project);
499 write_file(
500 &root,
501 "gleam.toml",
502 r#"
503name = "application"
504version = "1.0.0"
505
506[dependencies]
507hex_dep = ">= 1.0.0 and < 2.0.0"
508git_dep = ">= 1.0.0 and < 2.0.0"
509"#,
510 );
511 write_file(
512 &root,
513 "manifest.toml",
514 r#"
515packages = [
516 { name = "hex_dep", version = "1.0.0", build_tools = ["gleam"], requirements = ["local_dep"], source = "hex", outer_checksum = "00" },
517 { name = "git_dep", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://example.com/git_dep", commit = "0123456789abcdef" },
518 { name = "local_dep", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/local_dep" },
519]
520
521[requirements]
522"#,
523 );
524 write_file(
525 &root,
526 "src/main.gleam",
527 r#"
528import gleam
529import hex/nested
530
531pub fn main() {
532 nested.answer()
533}
534"#,
535 );
536 write_file(
537 &root,
538 "build/packages/hex_dep/gleam.toml",
539 r#"
540name = "hex_dep"
541version = "1.0.0"
542
543[dependencies]
544local_dep = { path = "../../../packages/local_dep" }
545"#,
546 );
547 write_file(
548 &root,
549 "build/packages/hex_dep/src/hex/nested.gleam",
550 r#"
551import local/value
552
553pub fn answer() {
554 value.answer()
555}
556"#,
557 );
558 write_file(
559 &root,
560 "build/packages/git_dep/gleam.toml",
561 r#"
562name = "git_dep"
563version = "1.0.0"
564
565[dependencies]
566local_dep = { path = "../../../packages/local_dep" }
567"#,
568 );
569 write_file(
570 &root,
571 "build/packages/git_dep/src/git/unselected.gleam",
572 "pub fn broken(",
573 );
574 write_file(
575 &root,
576 "packages/local_dep/gleam.toml",
577 r#"
578name = "local_dep"
579version = "1.0.0"
580"#,
581 );
582 write_file(
583 &root,
584 "packages/local_dep/src/local/value.gleam",
585 "pub fn answer() { 42 }",
586 );
587 write_file(&root, "src/README.md", "not a Gleam module");
588
589 let program =
590 compile_typed_project(root, "main").expect("resolved package project should compile");
591
592 assert_eq!(program.root_package(), "application");
593 assert_eq!(program.root_module(), "main");
594 assert_eq!(
595 program
596 .modules()
597 .map(|module| (module.type_info.package.as_str(), module.name.as_str()))
598 .collect::<Vec<_>>(),
599 [
600 ("local_dep", "local/value"),
601 ("hex_dep", "hex/nested"),
602 ("application", "main"),
603 ],
604 );
605 }
606
607 #[test]
608 fn loads_hosted_hex_git_and_local_modules_through_the_shared_project_owner() {
609 let project = tempdir().expect("temporary project should be created");
610 let root = project_root(&project);
611 write_file(
612 &root,
613 "gleam.toml",
614 r#"
615name = "application"
616version = "1.0.0"
617
618[dependencies]
619hex_dep = ">= 1.0.0 and < 2.0.0"
620git_dep = ">= 1.0.0 and < 2.0.0"
621"#,
622 );
623 write_file(
624 &root,
625 "manifest.toml",
626 r#"
627packages = [
628 { name = "hex_dep", version = "1.0.0", build_tools = ["gleam"], requirements = ["local_dep"], source = "hex", outer_checksum = "00" },
629 { name = "git_dep", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "git", repo = "https://example.com/git_dep", commit = "0123456789abcdef" },
630 { name = "local_dep", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/local_dep" },
631]
632
633[requirements]
634"#,
635 );
636 write_file(
637 &root,
638 "src/main.gleam",
639 r#"
640import git/value as git_value
641import hex/nested
642
643pub fn main() {
644 nested.answer() + git_value.answer()
645}
646"#,
647 );
648 write_file(
649 &root,
650 "build/packages/hex_dep/gleam.toml",
651 r#"
652name = "hex_dep"
653version = "1.0.0"
654
655[dependencies]
656local_dep = { path = "../../../packages/local_dep" }
657"#,
658 );
659 write_file(
660 &root,
661 "build/packages/hex_dep/src/hex/nested.gleam",
662 r#"
663import local/value
664
665pub fn answer() {
666 value.answer()
667}
668"#,
669 );
670 write_file(
671 &root,
672 "build/packages/git_dep/gleam.toml",
673 "name = \"git_dep\"\nversion = \"1.0.0\"\n",
674 );
675 write_file(
676 &root,
677 "build/packages/git_dep/src/git/value.gleam",
678 "pub fn answer() { 2 }",
679 );
680 write_file(
681 &root,
682 "packages/local_dep/gleam.toml",
683 "name = \"local_dep\"\nversion = \"1.0.0\"\n",
684 );
685 write_file(
686 &root,
687 "packages/local_dep/src/local/value.gleam",
688 r#"
689@external(erlang, "host", "answer")
690pub fn answer() -> Int
691"#,
692 );
693
694 let plain = compile_typed_project(root.clone(), "main")
695 .expect("plain project should share the selected source closure");
696 let provider = HostProviderModule::<StatelessHostProfile>::new("local_dep", "local/value")
697 .expect("provider module should be valid")
698 .with_function("answer", || BigInt::from(40))
699 .expect("provider function should be valid");
700 let hosted = compile_typed_host_project(
701 root.clone(),
702 "main",
703 HostProviderSet::with_providers(Vec::<HostModule>::new(), [provider])
704 .expect("provider module should be unique"),
705 )
706 .expect("hosted project should compile through the shared loader");
707 let plan = plan_host_program(hosted).expect("hosted project should plan");
708
709 let expected_modules = [
710 ("git_dep", "git/value"),
711 ("local_dep", "local/value"),
712 ("hex_dep", "hex/nested"),
713 ("application", "main"),
714 ];
715 assert_eq!(
716 plain
717 .modules()
718 .map(|module| (module.type_info.package.as_str(), module.name.as_str()))
719 .collect::<Vec<_>>(),
720 expected_modules,
721 );
722 assert_eq!(
723 plan.modules()
724 .iter()
725 .map(|module| (module.package().as_str(), module.module().as_str()))
726 .collect::<Vec<_>>(),
727 expected_modules,
728 );
729 assert_eq!(
730 plan.modules()
731 .iter()
732 .map(|module| {
733 module
734 .source_context()
735 .expect("filesystem modules should preserve source context")
736 .path()
737 .to_path_buf()
738 })
739 .collect::<Vec<_>>(),
740 [
741 root.join("build/packages/git_dep/src/git/value.gleam"),
742 root.join("packages/local_dep/src/local/value.gleam"),
743 root.join("build/packages/hex_dep/src/hex/nested.gleam"),
744 root.join("src/main.gleam"),
745 ],
746 );
747
748 let execution = HostedExecution::try_from_module_plan(plan)
749 .expect("hosted project execution should seal");
750 assert_eq!(
751 execution.run_main(&mut (), &mut Vec::new()),
752 Ok(Value::Int(42.into())),
753 );
754 }
755
756 #[test]
757 fn selects_only_provider_modules_in_the_resolved_source_closure() {
758 let project = tempdir().expect("temporary project should be created");
759 let root = project_root(&project);
760 write_file(
761 &root,
762 "gleam.toml",
763 "name = \"application\"\nversion = \"1.0.0\"\n",
764 );
765 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
766 write_file(
767 &root,
768 "src/main.gleam",
769 "import used\npub fn main() { used.value() }",
770 );
771 write_file(
772 &root,
773 "src/used.gleam",
774 r#"
775@external(erlang, "host", "value")
776pub fn value() -> Int
777"#,
778 );
779 write_file(
780 &root,
781 "src/unused.gleam",
782 r#"
783@external(erlang, "host", "value")
784pub fn value() -> Int
785"#,
786 );
787 let unused = HostProviderModule::<StatelessHostProfile>::new("application", "unused")
788 .expect("unused provider should be valid")
789 .with_function("value", BigInt::default)
790 .expect("unused provider function should be valid");
791 let selected = HostProviderModule::<StatelessHostProfile>::new("application", "used")
792 .expect("selected provider should be valid")
793 .with_function("value", || BigInt::from(42))
794 .expect("selected provider function should be valid");
795 let typed = compile_typed_host_project(
796 root,
797 "main",
798 HostProviderSet::with_providers(Vec::<HostModule>::new(), [unused, selected])
799 .expect("provider modules should be unique"),
800 )
801 .expect("unselected source providers should not enter the hosted program");
802 let plan = plan_host_program(typed).expect("selected provider should plan");
803 let execution = HostedExecution::try_from_module_plan(plan)
804 .expect("selected provider should seal for execution");
805
806 assert_eq!(
807 execution.run_main(&mut (), &mut Vec::new()),
808 Ok(Value::Int(42.into())),
809 );
810 }
811
812 #[test]
813 fn keeps_missing_project_providers_at_the_hosted_planner_boundary() {
814 let project = tempdir().expect("temporary project should be created");
815 let root = project_root(&project);
816 write_file(
817 &root,
818 "gleam.toml",
819 r#"
820name = "application"
821version = "1.0.0"
822
823[dependencies]
824library = { path = "packages/library" }
825"#,
826 );
827 write_file(
828 &root,
829 "manifest.toml",
830 r#"
831packages = [
832 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/library" },
833]
834
835[requirements]
836"#,
837 );
838 write_file(
839 &root,
840 "src/main.gleam",
841 "import support\npub fn main() { support.value() }",
842 );
843 write_file(
844 &root,
845 "packages/library/gleam.toml",
846 "name = \"library\"\nversion = \"1.0.0\"\n",
847 );
848 write_file(
849 &root,
850 "packages/library/src/support.gleam",
851 r#"
852@external(erlang, "host", "value")
853pub fn value() -> Int
854"#,
855 );
856
857 let typed = compile_typed_host_project(
858 root,
859 "main",
860 HostProviderSet::new(Vec::<HostModule>::new())
861 .expect("empty host modules should be valid"),
862 )
863 .expect("provider selection should remain a planning boundary");
864
865 assert_eq!(
866 plan_host_program(typed).err(),
867 Some(PlanError::MissingHostProvider {
868 package: "library".into(),
869 module: "support".into(),
870 function: "value".into(),
871 }),
872 );
873 }
874
875 #[test]
876 fn preserves_hosted_frontend_errors_in_project_errors() {
877 let project = tempdir().expect("temporary project should be created");
878 let root = project_root(&project);
879 write_file(
880 &root,
881 "gleam.toml",
882 "name = \"application\"\nversion = \"1.0.0\"\n",
883 );
884 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
885 write_file(&root, "src/main.gleam", "pub fn main() { 1 }");
886 let host = HostModule::new("host_support", "main").expect("host module should be valid");
887
888 let error = compile_typed_host_project(
889 root.clone(),
890 "main",
891 HostProviderSet::new([host]).expect("host module should be unique"),
892 )
893 .err()
894 .expect("source and host modules should still collide");
895
896 assert_eq!(
897 format!("{error:?}"),
898 format!(
899 "Frontend(SourceHostModuleCollision {{ module: \"main\", source_package: \"application\", source_path: {:?}, host_package: \"host_support\" }})",
900 root.join("src/main.gleam"),
901 ),
902 );
903 }
904
905 #[test]
906 fn excludes_root_dev_dependencies_from_the_resolved_program() {
907 let project = tempdir().expect("temporary project should be created");
908 let root = project_root(&project);
909 write_file(
910 &root,
911 "gleam.toml",
912 r#"
913name = "application"
914version = "1.0.0"
915
916[dev-dependencies]
917dev_only = ">= 1.0.0 and < 2.0.0"
918"#,
919 );
920 write_file(
921 &root,
922 "manifest.toml",
923 r#"
924packages = [
925 { name = "dev_only", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "00" },
926]
927
928[requirements]
929"#,
930 );
931 write_file(&root, "src/main.gleam", "pub fn main() { 1 }");
932
933 let program =
934 compile_typed_project(root, "main").expect("development dependency should be ignored");
935
936 assert_eq!(
937 program
938 .modules()
939 .map(|module| module.name.as_str())
940 .collect::<Vec<_>>(),
941 ["main"],
942 );
943 }
944
945 #[test]
946 fn skips_non_gleam_resolved_packages() {
947 let project = tempdir().expect("temporary project should be created");
948 let root = project_root(&project);
949 write_file(
950 &root,
951 "gleam.toml",
952 r#"
953name = "application"
954version = "1.0.0"
955
956[dependencies]
957build_only = ">= 1.0.0 and < 2.0.0"
958"#,
959 );
960 write_file(
961 &root,
962 "manifest.toml",
963 r#"
964packages = [
965 { name = "build_only", version = "1.0.0", build_tools = ["rebar3"], requirements = [], source = "hex", outer_checksum = "00" },
966]
967
968[requirements]
969"#,
970 );
971 write_file(&root, "src/main.gleam", "pub fn main() { 1 }");
972
973 let program =
974 compile_typed_project(root, "main").expect("non-Gleam package should be skipped");
975
976 assert_eq!(
977 program
978 .modules()
979 .map(|module| module.name.as_str())
980 .collect::<Vec<_>>(),
981 ["main"],
982 );
983 }
984
985 #[test]
986 fn ignores_unselected_modules_but_plans_every_selected_body() {
987 let project = tempdir().expect("temporary project should be created");
988 let root = project_root(&project);
989 write_file(
990 &root,
991 "gleam.toml",
992 r#"
993name = "application"
994version = "1.0.0"
995
996[dependencies]
997library = ">= 1.0.0 and < 2.0.0"
998"#,
999 );
1000 write_file(
1001 &root,
1002 "manifest.toml",
1003 r#"
1004packages = [
1005 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "00" },
1006]
1007
1008[requirements]
1009"#,
1010 );
1011 write_file(
1012 &root,
1013 "src/main.gleam",
1014 "import selected\npub fn main() { selected.value() }",
1015 );
1016 write_file(
1017 &root,
1018 "build/packages/library/gleam.toml",
1019 "name = \"library\"\nversion = \"1.0.0\"\n",
1020 );
1021 write_file(
1022 &root,
1023 "build/packages/library/src/selected.gleam",
1024 r#"
1025pub fn value() {
1026 1
1027}
1028
1029fn unsupported_unused_body() {
1030 <<1:native>>
1031}
1032"#,
1033 );
1034 write_file(
1035 &root,
1036 "build/packages/library/src/unselected.gleam",
1037 "pub fn broken(",
1038 );
1039
1040 let program =
1041 compile_typed_project(root, "main").expect("selected source closure should compile");
1042 assert_eq!(
1043 program
1044 .modules()
1045 .map(|module| module.name.as_str())
1046 .collect::<Vec<_>>(),
1047 ["selected", "main"],
1048 );
1049 assert_eq!(
1050 plan_program(program),
1051 Err(PlanError::UnsupportedBitArraySegment {
1052 reason: crate::planner::UnsupportedBitArraySegmentReason::NativeEndianness,
1053 }),
1054 );
1055 }
1056
1057 #[test]
1058 fn preserves_dependency_source_paths_in_module_plans() {
1059 let project = tempdir().expect("temporary project should be created");
1060 let root = project_root(&project);
1061 write_file(
1062 &root,
1063 "gleam.toml",
1064 r#"
1065name = "application"
1066version = "1.0.0"
1067
1068[dependencies]
1069library = { path = "packages/library" }
1070"#,
1071 );
1072 write_file(
1073 &root,
1074 "manifest.toml",
1075 r#"
1076packages = [
1077 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/library" },
1078]
1079
1080[requirements]
1081"#,
1082 );
1083 write_file(
1084 &root,
1085 "src/main.gleam",
1086 "import support\npub fn main() { support.value() }",
1087 );
1088 write_file(
1089 &root,
1090 "packages/library/gleam.toml",
1091 "name = \"library\"\nversion = \"1.0.0\"\n",
1092 );
1093 write_file(
1094 &root,
1095 "packages/library/src/support.gleam",
1096 "pub fn value() { 42 }",
1097 );
1098
1099 let plan = plan_program(
1100 compile_typed_project(root.clone(), "main")
1101 .expect("dependency source project should compile"),
1102 )
1103 .expect("dependency source project should plan");
1104
1105 assert_eq!(
1106 plan.modules()
1107 .iter()
1108 .map(|module| {
1109 (
1110 module.package().as_str(),
1111 module
1112 .source_context()
1113 .expect("filesystem module should preserve source context")
1114 .path()
1115 .as_str(),
1116 )
1117 })
1118 .collect::<Vec<_>>(),
1119 [
1120 (
1121 "library",
1122 root.join("packages/library/src/support.gleam").as_str(),
1123 ),
1124 ("application", root.join("src/main.gleam").as_str()),
1125 ],
1126 );
1127 }
1128
1129 #[test]
1130 fn keeps_selected_external_functions_at_the_existing_planner_boundary() {
1131 let project = tempdir().expect("temporary project should be created");
1132 let root = project_root(&project);
1133 write_file(
1134 &root,
1135 "gleam.toml",
1136 r#"
1137name = "application"
1138version = "1.0.0"
1139
1140[dependencies]
1141library = { path = "packages/library" }
1142"#,
1143 );
1144 write_file(
1145 &root,
1146 "manifest.toml",
1147 r#"
1148packages = [
1149 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/library" },
1150]
1151
1152[requirements]
1153"#,
1154 );
1155 write_file(
1156 &root,
1157 "src/main.gleam",
1158 "import support\npub fn main() { support.value() }",
1159 );
1160 write_file(
1161 &root,
1162 "packages/library/gleam.toml",
1163 "name = \"library\"\nversion = \"1.0.0\"\n",
1164 );
1165 write_file(
1166 &root,
1167 "packages/library/src/support.gleam",
1168 r#"
1169@external(erlang, "support", "value")
1170pub fn value() -> Int
1171"#,
1172 );
1173
1174 let program =
1175 compile_typed_project(root, "main").expect("external source should type-check");
1176
1177 assert_eq!(
1178 plan_program(program),
1179 Err(PlanError::UnsupportedFunction {
1180 name: "value".into(),
1181 reason: UnsupportedFunctionReason::External,
1182 }),
1183 );
1184 }
1185
1186 #[test]
1187 fn resolves_absolute_local_package_paths() {
1188 let project = tempdir().expect("temporary project should be created");
1189 let dependency = tempdir().expect("temporary dependency should be created");
1190 let root = project_root(&project);
1191 let dependency_root = project_root(&dependency);
1192 write_file(
1193 &root,
1194 "gleam.toml",
1195 r#"
1196name = "application"
1197version = "1.0.0"
1198
1199[dependencies]
1200library = { path = "../library" }
1201"#,
1202 );
1203 write_file(
1204 &root,
1205 "manifest.toml",
1206 &format!(
1207 r#"
1208packages = [
1209 {{ name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "{dependency_root}" }},
1210]
1211
1212[requirements]
1213"#,
1214 ),
1215 );
1216 write_file(
1217 &root,
1218 "src/main.gleam",
1219 "import support\npub fn main() { support.value() }",
1220 );
1221 write_file(
1222 &dependency_root,
1223 "gleam.toml",
1224 "name = \"library\"\nversion = \"1.0.0\"\n",
1225 );
1226 write_file(
1227 &dependency_root,
1228 "src/support.gleam",
1229 "pub fn value() { 42 }",
1230 );
1231
1232 let program =
1233 compile_typed_project(root, "main").expect("absolute local package should compile");
1234
1235 assert_eq!(
1236 program
1237 .modules()
1238 .map(|module| (module.type_info.package.as_str(), module.name.as_str()))
1239 .collect::<Vec<_>>(),
1240 [("library", "support"), ("application", "main")],
1241 );
1242 }
1243
1244 #[test]
1245 fn rejects_missing_package_config_for_plain_and_hosted_projects() {
1246 let project = tempdir().expect("temporary project should be created");
1247 let root = project_root(&project);
1248
1249 let plain_error = compile_typed_project(root.clone(), "main")
1250 .expect_err("missing package config should fail");
1251 let hosted_error = compile_typed_host_project(
1252 root.clone(),
1253 "main",
1254 HostProviderSet::new(Vec::<HostModule>::new())
1255 .expect("empty host modules should be valid"),
1256 )
1257 .err()
1258 .expect("missing package config should fail before hosted compilation");
1259 let expected = format!(
1260 "failed to read Gleam package config {}",
1261 root.join("gleam.toml"),
1262 );
1263
1264 assert_eq!(plain_error.to_string(), expected);
1265 assert_eq!(hosted_error.to_string(), expected);
1266 }
1267
1268 #[test]
1269 fn rejects_invalid_package_config() {
1270 let project = tempdir().expect("temporary project should be created");
1271 let root = project_root(&project);
1272 write_file(&root, "gleam.toml", "name = 1");
1273
1274 let error = compile_typed_project(root.clone(), "main")
1275 .expect_err("invalid package config should fail");
1276
1277 assert_eq!(
1278 error.to_string(),
1279 format!(
1280 "invalid Gleam package config {}: TOML parse error at line 1, column 8\n |\n1 | name = 1\n | ^\ninvalid type: integer `1`, expected a package name\n",
1281 root.join("gleam.toml"),
1282 ),
1283 );
1284 }
1285
1286 #[test]
1287 fn rejects_incompatible_embedded_compiler_versions() {
1288 let project = tempdir().expect("temporary project should be created");
1289 let root = project_root(&project);
1290 write_file(
1291 &root,
1292 "gleam.toml",
1293 r#"
1294name = "application"
1295version = "1.0.0"
1296gleam = ">= 99.0.0"
1297"#,
1298 );
1299
1300 let error = compile_typed_project(root, "main")
1301 .expect_err("incompatible Gleam compiler requirement should fail");
1302
1303 assert_eq!(
1304 error.to_string(),
1305 "package application is incompatible with the embedded Gleam compiler: The package application requires a Gleam version satisfying >=99.0.0 and you are using v1.18.1",
1306 );
1307 }
1308
1309 #[test]
1310 fn rejects_missing_manifests() {
1311 let project = tempdir().expect("temporary project should be created");
1312 let root = project_root(&project);
1313 write_file(
1314 &root,
1315 "gleam.toml",
1316 "name = \"application\"\nversion = \"1.0.0\"\n",
1317 );
1318
1319 let error =
1320 compile_typed_project(root.clone(), "main").expect_err("missing manifest should fail");
1321
1322 assert_eq!(
1323 error.to_string(),
1324 format!(
1325 "failed to read Gleam package manifest {}",
1326 root.join("manifest.toml"),
1327 ),
1328 );
1329 }
1330
1331 #[test]
1332 fn rejects_invalid_manifests() {
1333 let project = tempdir().expect("temporary project should be created");
1334 let root = project_root(&project);
1335 write_file(
1336 &root,
1337 "gleam.toml",
1338 "name = \"application\"\nversion = \"1.0.0\"\n",
1339 );
1340 write_file(&root, "manifest.toml", "packages = 1");
1341
1342 let error =
1343 compile_typed_project(root.clone(), "main").expect_err("invalid manifest should fail");
1344
1345 assert_eq!(
1346 error.to_string(),
1347 format!(
1348 "invalid Gleam package manifest {}: TOML parse error at line 1, column 12\n |\n1 | packages = 1\n | ^\ninvalid type: integer `1`, expected a sequence\n",
1349 root.join("manifest.toml"),
1350 ),
1351 );
1352 }
1353
1354 #[test]
1355 fn rejects_duplicate_manifest_packages() {
1356 let project = tempdir().expect("temporary project should be created");
1357 let root = project_root(&project);
1358 write_file(
1359 &root,
1360 "gleam.toml",
1361 "name = \"application\"\nversion = \"1.0.0\"\n",
1362 );
1363 write_file(
1364 &root,
1365 "manifest.toml",
1366 r#"
1367packages = [
1368 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "00" },
1369 { name = "library", version = "1.0.1", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "00" },
1370]
1371
1372[requirements]
1373"#,
1374 );
1375
1376 let error = compile_typed_project(root.clone(), "main")
1377 .expect_err("duplicate manifest package should fail");
1378
1379 assert_eq!(
1380 error.to_string(),
1381 format!(
1382 "invalid Gleam package manifest {}: package library is listed more than once",
1383 root.join("manifest.toml"),
1384 ),
1385 );
1386 }
1387
1388 #[test]
1389 fn rejects_dependencies_absent_from_the_manifest() {
1390 let project = tempdir().expect("temporary project should be created");
1391 let root = project_root(&project);
1392 write_file(
1393 &root,
1394 "gleam.toml",
1395 r#"
1396name = "application"
1397version = "1.0.0"
1398
1399[dependencies]
1400library = ">= 1.0.0 and < 2.0.0"
1401"#,
1402 );
1403 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
1404
1405 let error = compile_typed_project(root, "main")
1406 .expect_err("dependency absent from manifest should fail");
1407
1408 assert_eq!(
1409 error.to_string(),
1410 "package application requires library, but it is absent from manifest.toml",
1411 );
1412 }
1413
1414 #[test]
1415 fn rejects_undownloaded_resolved_packages() {
1416 let project = tempdir().expect("temporary project should be created");
1417 let root = project_root(&project);
1418 write_file(
1419 &root,
1420 "gleam.toml",
1421 r#"
1422name = "application"
1423version = "1.0.0"
1424
1425[dependencies]
1426library = ">= 1.0.0 and < 2.0.0"
1427"#,
1428 );
1429 write_file(
1430 &root,
1431 "manifest.toml",
1432 r#"
1433packages = [
1434 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "00" },
1435]
1436
1437[requirements]
1438"#,
1439 );
1440
1441 let error = compile_typed_project(root.clone(), "main")
1442 .expect_err("undownloaded package should fail");
1443
1444 assert_eq!(
1445 error.to_string(),
1446 format!(
1447 "resolved package library is not downloaded at {}; run `gleam deps download` first",
1448 root.join("build/packages/library"),
1449 ),
1450 );
1451 }
1452
1453 #[test]
1454 fn rejects_missing_dependency_configs() {
1455 let project = tempdir().expect("temporary project should be created");
1456 let root = project_root(&project);
1457 write_file(
1458 &root,
1459 "gleam.toml",
1460 r#"
1461name = "application"
1462version = "1.0.0"
1463
1464[dependencies]
1465library = { path = "packages/library" }
1466"#,
1467 );
1468 write_file(
1469 &root,
1470 "manifest.toml",
1471 r#"
1472packages = [
1473 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/library" },
1474]
1475
1476[requirements]
1477"#,
1478 );
1479
1480 let error = compile_typed_project(root.clone(), "main")
1481 .expect_err("missing dependency config should fail");
1482
1483 assert_eq!(
1484 error.to_string(),
1485 format!(
1486 "failed to read Gleam package config {}",
1487 root.join("packages/library/gleam.toml"),
1488 ),
1489 );
1490 }
1491
1492 #[test]
1493 fn rejects_dependency_package_name_mismatches() {
1494 let project = tempdir().expect("temporary project should be created");
1495 let root = project_root(&project);
1496 write_file(
1497 &root,
1498 "gleam.toml",
1499 r#"
1500name = "application"
1501version = "1.0.0"
1502
1503[dependencies]
1504library = ">= 1.0.0 and < 2.0.0"
1505"#,
1506 );
1507 write_file(
1508 &root,
1509 "manifest.toml",
1510 r#"
1511packages = [
1512 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "hex", outer_checksum = "00" },
1513]
1514
1515[requirements]
1516"#,
1517 );
1518 write_file(
1519 &root,
1520 "build/packages/library/gleam.toml",
1521 "name = \"different\"\nversion = \"1.0.0\"\n",
1522 );
1523
1524 let error = compile_typed_project(root.clone(), "main")
1525 .expect_err("dependency package name mismatch should fail");
1526
1527 assert_eq!(
1528 error.to_string(),
1529 format!(
1530 "manifest package library resolves to a package config named different at {}",
1531 root.join("build/packages/library/gleam.toml"),
1532 ),
1533 );
1534 }
1535
1536 #[test]
1537 fn rejects_missing_source_directories() {
1538 let project = tempdir().expect("temporary project should be created");
1539 let root = project_root(&project);
1540 write_file(
1541 &root,
1542 "gleam.toml",
1543 "name = \"application\"\nversion = \"1.0.0\"\n",
1544 );
1545 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
1546
1547 let error =
1548 compile_typed_project(root.clone(), "main").expect_err("missing source should fail");
1549
1550 assert_eq!(
1551 error.to_string(),
1552 format!("failed to read Gleam source {}", root.join("src")),
1553 );
1554 }
1555
1556 #[test]
1557 fn preserves_frontend_errors_for_missing_root_modules() {
1558 let project = tempdir().expect("temporary project should be created");
1559 let root = project_root(&project);
1560 write_file(
1561 &root,
1562 "gleam.toml",
1563 "name = \"application\"\nversion = \"1.0.0\"\n",
1564 );
1565 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
1566 write_file(&root, "src/other.gleam", "pub fn value() { 1 }");
1567
1568 let error =
1569 compile_typed_project(root, "main").expect_err("missing root module should fail");
1570
1571 assert_eq!(
1572 error.to_string(),
1573 "root module main was not supplied by package application",
1574 );
1575 }
1576
1577 #[test]
1578 fn preserves_selected_dependency_parse_paths() {
1579 let project = tempdir().expect("temporary project should be created");
1580 let root = project_root(&project);
1581 write_file(
1582 &root,
1583 "gleam.toml",
1584 r#"
1585name = "application"
1586version = "1.0.0"
1587
1588[dependencies]
1589library = { path = "packages/library" }
1590"#,
1591 );
1592 write_file(
1593 &root,
1594 "manifest.toml",
1595 r#"
1596packages = [
1597 { name = "library", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/library" },
1598]
1599
1600[requirements]
1601"#,
1602 );
1603 write_file(
1604 &root,
1605 "src/main.gleam",
1606 "import support\npub fn main() { 1 }",
1607 );
1608 write_file(
1609 &root,
1610 "packages/library/gleam.toml",
1611 "name = \"library\"\nversion = \"1.0.0\"\n",
1612 );
1613 write_file(
1614 &root,
1615 "packages/library/src/support.gleam",
1616 "pub fn broken(",
1617 );
1618
1619 let error = compile_typed_project(root.clone(), "main")
1620 .expect_err("selected dependency parse failure should be preserved");
1621
1622 assert_eq!(
1623 error.to_string(),
1624 format!(
1625 "failed to parse Gleam module {}",
1626 root.join("packages/library/src/support.gleam"),
1627 ),
1628 );
1629 }
1630
1631 #[test]
1632 fn preserves_selected_module_import_cycles() {
1633 let project = tempdir().expect("temporary project should be created");
1634 let root = project_root(&project);
1635 write_file(
1636 &root,
1637 "gleam.toml",
1638 "name = \"application\"\nversion = \"1.0.0\"\n",
1639 );
1640 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
1641 write_file(
1642 &root,
1643 "src/main.gleam",
1644 "import support\npub fn main() { support.value() }",
1645 );
1646 write_file(
1647 &root,
1648 "src/support.gleam",
1649 "import main\npub fn value() { main.main() }",
1650 );
1651
1652 let error =
1653 compile_typed_project(root, "main").expect_err("module import cycle should fail");
1654
1655 assert_eq!(
1656 format!("{error:?}"),
1657 "Frontend(ImportCycle { modules: [\"main\", \"support\", \"main\"] })",
1658 );
1659 }
1660
1661 #[test]
1662 fn rejects_selected_duplicate_module_owners() {
1663 let project = tempdir().expect("temporary project should be created");
1664 let root = project_root(&project);
1665 write_file(
1666 &root,
1667 "gleam.toml",
1668 r#"
1669name = "application"
1670version = "1.0.0"
1671
1672[dependencies]
1673first = { path = "packages/first" }
1674second = { path = "packages/second" }
1675"#,
1676 );
1677 write_file(
1678 &root,
1679 "manifest.toml",
1680 r#"
1681packages = [
1682 { name = "first", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/first" },
1683 { name = "second", version = "1.0.0", build_tools = ["gleam"], requirements = [], source = "local", path = "packages/second" },
1684]
1685
1686[requirements]
1687"#,
1688 );
1689 write_file(
1690 &root,
1691 "src/main.gleam",
1692 "import shared\npub fn main() { shared.value() }",
1693 );
1694 write_file(
1695 &root,
1696 "packages/first/gleam.toml",
1697 "name = \"first\"\nversion = \"1.0.0\"\n",
1698 );
1699 write_file(
1700 &root,
1701 "packages/first/src/shared.gleam",
1702 "pub fn value() { 1 }",
1703 );
1704 write_file(
1705 &root,
1706 "packages/second/gleam.toml",
1707 "name = \"second\"\nversion = \"1.0.0\"\n",
1708 );
1709 write_file(
1710 &root,
1711 "packages/second/src/shared.gleam",
1712 "pub fn value() { 2 }",
1713 );
1714
1715 let error = compile_typed_project(root.clone(), "main")
1716 .expect_err("duplicate selected module owners should fail");
1717
1718 assert_eq!(
1719 format!("{error:?}"),
1720 format!(
1721 "Frontend(DuplicateModule {{ module: \"shared\", first_package: \"first\", first_path: \"{}\", second_package: \"second\", second_path: \"{}\" }})",
1722 root.join("packages/first/src/shared.gleam"),
1723 root.join("packages/second/src/shared.gleam"),
1724 ),
1725 );
1726 }
1727
1728 #[test]
1729 fn leaves_unknown_imports_to_gleam_analysis() {
1730 let project = tempdir().expect("temporary project should be created");
1731 let root = project_root(&project);
1732 write_file(
1733 &root,
1734 "gleam.toml",
1735 "name = \"application\"\nversion = \"1.0.0\"\n",
1736 );
1737 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
1738 write_file(
1739 &root,
1740 "src/main.gleam",
1741 "import unknown\npub fn main() { 1 }",
1742 );
1743
1744 let error =
1745 compile_typed_project(root, "main").expect_err("unknown import should fail analysis");
1746
1747 assert_eq!(error.to_string(), "failed to analyse Gleam module");
1748 }
1749
1750 #[cfg(unix)]
1751 #[test]
1752 fn rejects_non_utf8_source_paths() {
1753 let error = source_paths_from(
1754 &NonUtf8SourceDirectoryEntry,
1755 Utf8Path::new("src"),
1756 Utf8Path::new(""),
1757 )
1758 .expect_err("non-UTF-8 path should fail");
1759
1760 assert_eq!(error.to_string(), "failed to read Gleam source src");
1761 }
1762
1763 #[test]
1764 fn rejects_failed_source_directory_entries() {
1765 let error = source_paths_from(
1766 &FailedSourceDirectoryEntry,
1767 Utf8Path::new("src"),
1768 Utf8Path::new(""),
1769 )
1770 .expect_err("failed directory entry should fail");
1771
1772 assert_eq!(error.to_string(), "failed to read Gleam source src");
1773 }
1774
1775 #[cfg(unix)]
1776 #[test]
1777 fn rejects_unreadable_selected_source_files() {
1778 use std::os::unix::fs::PermissionsExt;
1779
1780 let project = tempdir().expect("temporary project should be created");
1781 let root = project_root(&project);
1782 write_file(
1783 &root,
1784 "gleam.toml",
1785 "name = \"application\"\nversion = \"1.0.0\"\n",
1786 );
1787 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
1788 write_file(&root, "src/main.gleam", "pub fn main() { 1 }");
1789 let source_path = root.join("src/main.gleam");
1790 let mut permissions = fs::metadata(&source_path)
1791 .expect("source metadata should be readable")
1792 .permissions();
1793 permissions.set_mode(0o000);
1794 fs::set_permissions(&source_path, permissions)
1795 .expect("source permissions should be changed");
1796
1797 let error = compile_typed_project(root, "main")
1798 .expect_err("unreadable selected source should fail");
1799
1800 let mut permissions = fs::metadata(&source_path)
1801 .expect("source metadata should remain readable")
1802 .permissions();
1803 permissions.set_mode(0o600);
1804 fs::set_permissions(&source_path, permissions)
1805 .expect("source permissions should be restored");
1806 assert_eq!(
1807 error.to_string(),
1808 format!("failed to read Gleam source {source_path}"),
1809 );
1810 }
1811
1812 #[cfg(unix)]
1813 #[test]
1814 fn rejects_unreadable_nested_source_directories() {
1815 use std::os::unix::fs::PermissionsExt;
1816
1817 let project = tempdir().expect("temporary project should be created");
1818 let root = project_root(&project);
1819 write_file(
1820 &root,
1821 "gleam.toml",
1822 "name = \"application\"\nversion = \"1.0.0\"\n",
1823 );
1824 write_file(&root, "manifest.toml", "packages = []\n\n[requirements]\n");
1825 write_file(&root, "src/main.gleam", "pub fn main() { 1 }");
1826 write_file(&root, "src/private/hidden.gleam", "pub fn value() { 1 }");
1827 let private_path = root.join("src/private");
1828 let mut permissions = fs::metadata(&private_path)
1829 .expect("source directory metadata should be readable")
1830 .permissions();
1831 permissions.set_mode(0o000);
1832 fs::set_permissions(&private_path, permissions)
1833 .expect("source directory permissions should be changed");
1834
1835 let error = compile_typed_project(root, "main")
1836 .expect_err("unreadable nested source directory should fail");
1837
1838 let mut permissions = fs::metadata(&private_path)
1839 .expect("source directory metadata should remain readable")
1840 .permissions();
1841 permissions.set_mode(0o700);
1842 fs::set_permissions(&private_path, permissions)
1843 .expect("source directory permissions should be restored");
1844 assert_eq!(
1845 error.to_string(),
1846 format!("failed to read Gleam source {private_path}"),
1847 );
1848 }
1849
1850 fn project_root(project: &TempDir) -> Utf8PathBuf {
1851 Utf8PathBuf::from_path_buf(project.path().to_path_buf())
1852 .expect("temporary project path should be UTF-8")
1853 }
1854
1855 fn write_file(root: &Utf8Path, relative: &str, source: &str) {
1856 let path = root.join(relative);
1857 fs::create_dir_all(path.parent().expect("fixture file should have a parent"))
1858 .expect("fixture directory should be created");
1859 fs::write(path, source).expect("fixture file should be written");
1860 }
1861}