Fork of daniellemaywood.uk/gleam — Wasm codegen work
30 kB
936 lines
1use camino::{Utf8Path, Utf8PathBuf};
2use ecow::EcoString;
3use flate2::{Compression, write::GzEncoder};
4use gleam_core::{
5 Error, Result,
6 analyse::TargetSupport,
7 build::{Codegen, Compile, Mode, Options, Package, Target},
8 config::{GleamVersion, PackageConfig, SpdxLicense},
9 docs::{Dependency, DependencyKind, DocContext},
10 error::{SmallVersion, wrap},
11 hex,
12 manifest::ManifestPackageSource,
13 paths::{self, ProjectPaths},
14 requirement::Requirement,
15 type_,
16};
17use hexpm::version::{Range, Version};
18use itertools::Itertools;
19use sha2::Digest;
20use std::{collections::HashMap, io::Write, path::PathBuf, time::Instant};
21
22use crate::{build, cli, docs, fs, http::HttpClient};
23
24pub fn command(paths: &ProjectPaths, replace: bool, i_am_sure: bool) -> Result<()> {
25 let mut config = crate::config::root_config(paths)?;
26
27 let should_publish = check_for_gleam_prefix(&config)?
28 && check_for_version_zero(&config)?
29 && check_repo_url(&config, i_am_sure)?;
30
31 if !should_publish {
32 println!("Not publishing.");
33 return Ok(());
34 }
35
36 let Tarball {
37 mut compile_result,
38 cached_modules,
39 data: package_tarball,
40 src_files_added,
41 generated_files_added,
42 dependencies,
43 } = do_build_hex_tarball(paths, &mut config)?;
44
45 check_for_name_squatting(&compile_result)?;
46 check_for_multiple_top_level_modules(&compile_result, i_am_sure)?;
47
48 // Build HTML documentation
49 let docs_tarball = fs::create_tar_archive(docs::build_documentation(
50 paths,
51 &config,
52 dependencies,
53 &mut compile_result,
54 DocContext::HexPublish,
55 &cached_modules,
56 )?)?;
57
58 // Ask user if this is correct
59 if !generated_files_added.is_empty() {
60 println!("\nGenerated files:");
61 for file in generated_files_added.iter().sorted() {
62 println!(" - {}", file.0);
63 }
64 }
65 println!("\nSource files:");
66 for file in src_files_added.iter().sorted() {
67 println!(" - {file}");
68 }
69 println!("\nName: {}", config.name);
70 println!("Version: {}", config.version);
71
72 let should_publish = i_am_sure || cli::confirm("\nDo you wish to publish this package?")?;
73 if !should_publish {
74 println!("Not publishing.");
75 return Ok(());
76 }
77
78 let runtime = tokio::runtime::Runtime::new().expect("Unable to start Tokio async runtime");
79 let hex_config = hexpm::Config::new();
80 let api_key =
81 crate::hex::HexAuthentication::new(&runtime, hex_config.clone()).get_or_create_api_key()?;
82 let start = Instant::now();
83 cli::print_publishing(&config.name, &config.version);
84
85 runtime.block_on(hex::publish_package(
86 package_tarball,
87 config.version.to_string(),
88 &api_key,
89 &hex_config,
90 replace,
91 &HttpClient::new(),
92 ))?;
93
94 cli::print_publishing_documentation();
95 runtime.block_on(hex::publish_documentation(
96 &config.name,
97 &config.version,
98 docs_tarball,
99 &api_key,
100 &hex_config,
101 &HttpClient::new(),
102 ))?;
103 cli::print_published(start.elapsed());
104 println!(
105 "\nView your package at https://hex.pm/packages/{}",
106 &config.name
107 );
108
109 // Prompt the user to make a git tag if they have not.
110 let has_repo = config.repository.is_some();
111 let git = PathBuf::from(".git");
112 let tag_name = config.tag_for_version(&config.version);
113 let git_tag = git.join("refs").join("tags").join(&tag_name);
114 if has_repo && git.exists() && !git_tag.exists() {
115 println!(
116 "
117Please push a git tag for this release so source code links in the
118HTML documentation will work:
119
120 git tag {tag_name}
121 git push origin {tag_name}
122"
123 )
124 }
125 Ok(())
126}
127
128fn check_for_name_squatting(package: &Package) -> Result<(), Error> {
129 if package.modules.len() > 1 {
130 return Ok(());
131 }
132
133 let Some(module) = package.modules.first() else {
134 return Err(Error::HexPackageSquatting);
135 };
136
137 if module.dependencies.len() > 1 {
138 return Ok(());
139 }
140
141 let definitions = &module.ast.definitions;
142
143 if definitions.len() > 2 {
144 return Ok(());
145 }
146
147 let Some(main) = definitions.iter().find_map(|d| d.main_function()) else {
148 return Ok(());
149 };
150
151 if main.body.first().is_println() {
152 return Err(Error::HexPackageSquatting);
153 }
154
155 Ok(())
156}
157
158fn check_for_multiple_top_level_modules(package: &Package, i_am_sure: bool) -> Result<(), Error> {
159 // Collect top-level module names
160 let mut top_level_module_names = package
161 .modules
162 .iter()
163 .filter_map(|module| {
164 // Top-level modules are those that don't contain any path separators
165 if module.name.contains('/') {
166 None
167 } else {
168 Some(module.name.clone())
169 }
170 })
171 .collect::<Vec<_>>();
172
173 // Remove duplicates
174 top_level_module_names.sort_unstable();
175 top_level_module_names.dedup();
176
177 // If more than one top-level module name is found, prompt for confirmation
178 if top_level_module_names.len() > 1 {
179 let text = wrap(&format!(
180 "Your package defines multiple top-level modules: {}.
181
182Defining multiple top-level modules can lead to namespace pollution \
183and potential conflicts for consumers.
184
185To fix this, move all your modules under a single top-level module of your choice.
186
187For example:
188 src/{1}.gleam
189 src/{1}/module1.gleam
190 src/{1}/module2.gleam",
191 top_level_module_names.join(", "),
192 package.config.name
193 ));
194 println!("{text}\n");
195
196 let should_publish =
197 i_am_sure || cli::confirm("\nDo you wish to continue publishing this package?")?;
198 println!();
199
200 if !should_publish {
201 println!("Not publishing.");
202 std::process::exit(0);
203 }
204 }
205
206 Ok(())
207}
208
209fn check_repo_url(config: &PackageConfig, i_am_sure: bool) -> Result<bool, Error> {
210 let Some(repo) = config.repository.as_ref() else {
211 return Ok(true);
212 };
213 let url = repo.url();
214
215 let runtime = tokio::runtime::Runtime::new().expect("Unable to start Tokio async runtime");
216 let response = runtime.block_on(reqwest::get(&url)).map_err(Error::http)?;
217
218 if response.status().is_success() {
219 return Ok(true);
220 }
221
222 println!(
223 "The repository configuration in your `gleam.toml` file does not appear to be
224valid, {} returned status {}",
225 &url,
226 response.status()
227 );
228 let should_publish = i_am_sure || cli::confirm("\nDo you wish to continue?")?;
229 println!();
230 Ok(should_publish)
231}
232
233/// Ask for confirmation if the package name if a v0.x.x version
234fn check_for_version_zero(config: &PackageConfig) -> Result<bool, Error> {
235 if config.version.major != 0 {
236 return Ok(true);
237 }
238
239 println!(
240 "You are about to publish a release that is below version 1.0.0.
241
242Semantic versioning doesn't apply to version 0.x.x releases, so your
243users will not be protected from breaking changes. This can result
244in a poor user experience where packages can break unexpectedly with
245updates that would normally be safe.
246
247If your package is not ready to be used in production it should not
248be published.
249\n"
250 );
251 let should_publish = cli::confirm_with_text("I am not using semantic versioning")?;
252 println!();
253 Ok(should_publish)
254}
255
256/// Ask for confirmation if the package name if `gleam_*`
257fn check_for_gleam_prefix(config: &PackageConfig) -> Result<bool, Error> {
258 if !config.name.starts_with("gleam_") || config.name.starts_with("gleam_community_") {
259 return Ok(true);
260 }
261
262 println!(
263 "You are about to publish a package with a name that starts with
264the prefix `gleam_`, which is for packages maintained by the Gleam
265core team.\n",
266 );
267 let should_publish = cli::confirm_with_text("I am part of the Gleam core team")?;
268 println!();
269 Ok(should_publish)
270}
271
272struct Tarball {
273 compile_result: Package,
274 cached_modules: im::HashMap<EcoString, type_::ModuleInterface>,
275 data: Vec<u8>,
276 src_files_added: Vec<Utf8PathBuf>,
277 generated_files_added: Vec<(Utf8PathBuf, String)>,
278 dependencies: HashMap<EcoString, Dependency>,
279}
280
281pub fn build_hex_tarball(paths: &ProjectPaths, config: &mut PackageConfig) -> Result<Vec<u8>> {
282 let Tarball { data, .. } = do_build_hex_tarball(paths, config)?;
283 Ok(data)
284}
285
286fn do_build_hex_tarball(paths: &ProjectPaths, config: &mut PackageConfig) -> Result<Tarball> {
287 let target = config.target;
288 check_config_for_publishing(config)?;
289
290 // Reset the build directory so we know the state of the project
291 fs::delete_directory(&paths.build_directory_for_target(Mode::Prod, target))?;
292
293 let manifest = build::download_dependencies(paths, cli::Reporter::new())?;
294 let dependencies = manifest
295 .packages
296 .iter()
297 .map(|package| {
298 (
299 package.name.clone(),
300 Dependency {
301 version: package.version.clone(),
302 kind: match &package.source {
303 ManifestPackageSource::Hex { .. } => DependencyKind::Hex,
304 ManifestPackageSource::Git { .. } => DependencyKind::Git,
305 ManifestPackageSource::Local { .. } => DependencyKind::Path,
306 },
307 },
308 )
309 })
310 .collect();
311
312 // Build the project to check that it is valid
313 let built = build::main(
314 paths,
315 Options {
316 root_target_support: TargetSupport::Enforced,
317 warnings_as_errors: false,
318 mode: Mode::Prod,
319 target: Some(target),
320 codegen: Codegen::All,
321 compile: Compile::All,
322 no_print_progress: false,
323 },
324 manifest,
325 )?;
326
327 let minimum_required_version = built.minimum_required_version();
328 match &config.gleam_version {
329 // If the package has no explicit `gleam` version in its `gleam.toml`
330 // then we want to add the automatically inferred one so we know it's
331 // correct and folks getting the package from Hex won't have unpleasant
332 // surprises if the author forgot to manualy write it down.
333 None => {
334 // If we're automatically adding the minimum required version
335 // constraint we want it to at least be `>= 1.0.0`, even if the
336 // inferred lower bound could be lower.
337 let minimum_required_version =
338 std::cmp::max(minimum_required_version, Version::new(1, 0, 0));
339 let inferred_version_range = pubgrub::Range::higher_than(minimum_required_version);
340 config.gleam_version = Some(GleamVersion::from_pubgrub(inferred_version_range));
341 }
342 // Otherwise we need to check that the annotated version range is
343 // correct and includes the minimum required version.
344 Some(gleam_version) => {
345 if let Some(lowest_allowed_version) = gleam_version.lowest_version()
346 && lowest_allowed_version < minimum_required_version
347 {
348 return Err(Error::CannotPublishWrongVersion {
349 minimum_required_version: SmallVersion::from_hexpm(minimum_required_version),
350 wrongfully_allowed_version: SmallVersion::from_hexpm(lowest_allowed_version),
351 });
352 }
353 }
354 }
355
356 // If any of the modules in the package contain a todo or an echo then
357 // refuse to publish as the package is not yet finished.
358 let mut modules_containing_todo = vec![];
359 let mut modules_containing_echo = vec![];
360
361 for module in built.root_package.modules.iter() {
362 if module.ast.type_info.contains_todo() {
363 modules_containing_todo.push(module.name.clone());
364 } else if module.ast.type_info.contains_echo {
365 modules_containing_echo.push(module.name.clone());
366 }
367 }
368
369 if !modules_containing_todo.is_empty() {
370 return Err(Error::CannotPublishTodo {
371 unfinished: modules_containing_todo,
372 });
373 }
374
375 if !modules_containing_echo.is_empty() {
376 return Err(Error::CannotPublishEcho {
377 unfinished: modules_containing_echo,
378 });
379 }
380
381 // TODO: If any of the modules in the package contain a leaked internal type then
382 // refuse to publish as the package is not yet finished.
383 // We need to move aliases in to the type system first.
384 // context: https://discord.com/channels/768594524158427167/768594524158427170/1227250677734969386
385
386 // Collect all the files we want to include in the tarball
387 let generated_files = match target {
388 Target::Erlang => generated_erlang_files(paths, &built.root_package)?,
389 Target::JavaScript => vec![],
390 };
391 let src_files = project_files(Utf8Path::new(""))?;
392 let contents_tar_gz = contents_tarball(&src_files, &generated_files)?;
393 let version = "3";
394 let metadata = metadata_config(&built.root_package.config, &src_files, &generated_files)?;
395
396 // Calculate checksum
397 let mut hasher = sha2::Sha256::new();
398 hasher.update(version.as_bytes());
399 hasher.update(metadata.as_bytes());
400 hasher.update(contents_tar_gz.as_slice());
401 let checksum = base16::encode_upper(&hasher.finalize());
402 tracing::info!(checksum = %checksum, "Generated Hex package inner checksum");
403
404 // Build tarball
405 let mut tarball = Vec::new();
406 {
407 let mut tarball = tar::Builder::new(&mut tarball);
408 add_to_tar(&mut tarball, "VERSION", version.as_bytes())?;
409 add_to_tar(&mut tarball, "metadata.config", metadata.as_bytes())?;
410 add_to_tar(&mut tarball, "contents.tar.gz", contents_tar_gz.as_slice())?;
411 add_to_tar(&mut tarball, "CHECKSUM", checksum.as_bytes())?;
412 tarball.finish().map_err(Error::finish_tar)?;
413 }
414 tracing::info!("Generated package Hex release tarball");
415 Ok(Tarball {
416 compile_result: built.root_package,
417 cached_modules: built.module_interfaces,
418 data: tarball,
419 src_files_added: src_files,
420 generated_files_added: generated_files,
421 dependencies,
422 })
423}
424
425fn check_config_for_publishing(config: &PackageConfig) -> Result<()> {
426 // These fields are required to publish a Hex package. Hex will reject
427 // packages without them.
428 if config.description.is_empty() || config.licences.is_empty() {
429 Err(Error::MissingHexPublishFields {
430 description_missing: config.description.is_empty(),
431 licence_missing: config.licences.is_empty(),
432 })
433 } else {
434 Ok(())
435 }
436}
437
438fn metadata_config<'a>(
439 config: &'a PackageConfig,
440 source_files: &[Utf8PathBuf],
441 generated_files: &[(Utf8PathBuf, String)],
442) -> Result<String> {
443 let repo_url = http::Uri::try_from(
444 config
445 .repository
446 .as_ref()
447 .map(|r| r.url())
448 .unwrap_or_default(),
449 )
450 .ok();
451 let requirements: Result<Vec<ReleaseRequirement<'a>>> = config
452 .dependencies
453 .iter()
454 .map(|(name, requirement)| match requirement {
455 Requirement::Hex { version } => Ok(ReleaseRequirement {
456 name,
457 requirement: version,
458 }),
459 _ => Err(Error::PublishNonHexDependencies {
460 package: name.to_string(),
461 }),
462 })
463 .collect();
464 let metadata = ReleaseMetadata {
465 name: &config.name,
466 version: &config.version,
467 description: &config.description,
468 source_files,
469 generated_files,
470 licenses: &config.licences,
471 links: config
472 .links
473 .iter()
474 .map(|l| (l.title.as_str(), l.href.clone()))
475 .chain(repo_url.into_iter().map(|u| ("Repository", u)))
476 .collect(),
477 requirements: requirements?,
478 build_tools: vec!["gleam"],
479 }
480 .as_erlang();
481 tracing::info!(contents = ?metadata, "Generated Hex metadata.config");
482 Ok(metadata)
483}
484
485fn contents_tarball(
486 files: &[Utf8PathBuf],
487 data_files: &[(Utf8PathBuf, String)],
488) -> Result<Vec<u8>, Error> {
489 let mut contents_tar_gz = Vec::new();
490 {
491 let mut tarball =
492 tar::Builder::new(GzEncoder::new(&mut contents_tar_gz, Compression::default()));
493 for path in files {
494 add_path_to_tar(&mut tarball, path)?;
495 }
496 for (path, contents) in data_files {
497 add_to_tar(&mut tarball, path, contents.as_bytes())?;
498 }
499 tarball.finish().map_err(Error::finish_tar)?;
500 }
501 tracing::info!("Generated contents.tar.gz");
502 Ok(contents_tar_gz)
503}
504
505fn project_files(base_path: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
506 let src = base_path.join(Utf8Path::new("src"));
507 let mut files: Vec<Utf8PathBuf> = fs::gleam_files(&src)
508 .chain(fs::native_files(&src))
509 .collect();
510 let private = base_path.join(Utf8Path::new("priv"));
511 let mut private_files: Vec<Utf8PathBuf> = fs::private_files(&private).collect();
512 files.append(&mut private_files);
513 let mut add = |path| {
514 let path = base_path.join(path);
515 if path.exists() {
516 files.push(path);
517 }
518 };
519 add("README");
520 add("README.md");
521 add("README.txt");
522 add("gleam.toml");
523 add("LICENSE");
524 add("LICENCE");
525 add("LICENSE.md");
526 add("LICENCE.md");
527 add("LICENSE.txt");
528 add("LICENCE.txt");
529 add("NOTICE");
530 add("NOTICE.md");
531 add("NOTICE.txt");
532 Ok(files)
533}
534
535// TODO: test
536fn generated_erlang_files(
537 paths: &ProjectPaths,
538 package: &Package,
539) -> Result<Vec<(Utf8PathBuf, String)>> {
540 let mut files = vec![];
541
542 let dir = paths.build_directory_for_package(Mode::Prod, Target::Erlang, &package.config.name);
543 let ebin = dir.join("ebin");
544 let build = dir.join(paths::ARTEFACT_DIRECTORY_NAME);
545 let include = dir.join("include");
546
547 let tar_src = Utf8Path::new("src");
548 let tar_include = Utf8Path::new("include");
549
550 // Erlang modules
551 for module in &package.modules {
552 // Do not publish test/ and dev/ code
553 if !module.origin.is_src() {
554 continue;
555 }
556
557 let name = module.compiled_erlang_path();
558 files.push((tar_src.join(&name), fs::read(build.join(name))?));
559 }
560
561 // Erlang headers
562 if include.is_dir() {
563 for file in fs::erlang_files(&include) {
564 let name = file.file_name().expect("generated_files include file name");
565 files.push((tar_include.join(name), fs::read(file)?));
566 }
567 }
568
569 // src/package.app.src file
570 let app = format!("{}.app", &package.config.name);
571 let appsrc = format!("{}.src", &app);
572 files.push((tar_src.join(appsrc), fs::read(ebin.join(app))?));
573
574 Ok(files)
575}
576
577fn add_to_tar<P, W>(tarball: &mut tar::Builder<W>, path: P, data: &[u8]) -> Result<()>
578where
579 P: AsRef<Utf8Path>,
580 W: Write,
581{
582 let path = path.as_ref();
583 tracing::info!(file=?path, "Adding file to tarball");
584 let mut header = tar::Header::new_gnu();
585 header.set_mode(0o600);
586 header.set_size(data.len() as u64);
587 header.set_cksum();
588 tarball
589 .append_data(&mut header, path, data)
590 .map_err(|e| Error::add_tar(path, e))
591}
592
593fn add_path_to_tar<P, W>(tarball: &mut tar::Builder<W>, path: P) -> Result<()>
594where
595 P: AsRef<Utf8Path>,
596 W: Write,
597{
598 let path = path.as_ref();
599 tracing::info!(file=?path, "Adding file to tarball");
600 tarball
601 .append_path(path)
602 .map_err(|e| Error::add_tar(path, e))
603}
604
605#[derive(Debug, Clone)]
606pub struct ReleaseMetadata<'a> {
607 name: &'a str,
608 version: &'a Version,
609 description: &'a str,
610 source_files: &'a [Utf8PathBuf],
611 generated_files: &'a [(Utf8PathBuf, String)],
612 licenses: &'a Vec<SpdxLicense>,
613 links: Vec<(&'a str, http::Uri)>,
614 requirements: Vec<ReleaseRequirement<'a>>,
615 build_tools: Vec<&'a str>,
616 // What should this be? I can't find it in the API anywhere.
617 // extra: (kvlist(string => kvlist(...))) (optional)
618}
619
620impl ReleaseMetadata<'_> {
621 pub fn as_erlang(&self) -> String {
622 fn link(link: &(&str, http::Uri)) -> String {
623 format!(
624 "\n {{<<\"{name}\">>, <<\"{url}\">>}}",
625 name = link.0,
626 url = link.1
627 )
628 }
629 fn file(name: impl AsRef<Utf8Path>) -> String {
630 format!("\n <<\"{name}\">>", name = name.as_ref())
631 }
632
633 format!(
634 r#"{{<<"name">>, <<"{name}">>}}.
635{{<<"app">>, <<"{name}">>}}.
636{{<<"version">>, <<"{version}">>}}.
637{{<<"description">>, <<"{description}"/utf8>>}}.
638{{<<"licenses">>, [{licenses}]}}.
639{{<<"build_tools">>, [{build_tools}]}}.
640{{<<"links">>, [{links}
641]}}.
642{{<<"requirements">>, [{requirements}
643]}}.
644{{<<"files">>, [{files}
645]}}.
646"#,
647 name = self.name,
648 version = self.version,
649 description = self.description,
650 files = self
651 .source_files
652 .iter()
653 .chain(self.generated_files.iter().map(|(p, _)| p))
654 .map(file)
655 .sorted()
656 .join(","),
657 links = self.links.iter().map(link).join(","),
658 licenses = self.licenses.iter().map(|l| quotes(l.as_ref())).join(", "),
659 build_tools = self.build_tools.iter().map(|l| quotes(l)).join(", "),
660 requirements = self
661 .requirements
662 .iter()
663 .map(ReleaseRequirement::as_erlang)
664 .join(",")
665 )
666 }
667}
668
669#[derive(Debug, Clone)]
670struct ReleaseRequirement<'a> {
671 name: &'a str,
672 // optional: bool,
673 requirement: &'a Range,
674 // Support alternate repositories at a later date.
675 // repository: String,
676}
677impl ReleaseRequirement<'_> {
678 pub fn as_erlang(&self) -> String {
679 format!(
680 r#"
681 {{<<"{app}">>, [
682 {{<<"app">>, <<"{app}">>}},
683 {{<<"optional">>, false}},
684 {{<<"requirement">>, <<"{requirement}">>}}
685 ]}}"#,
686 app = self.name,
687 requirement = self.requirement,
688 )
689 }
690}
691
692#[test]
693fn release_metadata_as_erlang() {
694 let licences = vec![
695 SpdxLicense {
696 licence: "MIT".into(),
697 },
698 SpdxLicense {
699 licence: "MPL-2.0".into(),
700 },
701 ];
702 let version = "1.2.3".try_into().unwrap();
703 let homepage = "https://gleam.run".parse().unwrap();
704 let github = "https://github.com/lpil/myapp".parse().unwrap();
705 let req1 = Range::new("~> 1.2.3 or >= 5.0.0".into()).unwrap();
706 let req2 = Range::new("~> 1.2".into()).unwrap();
707 let meta = ReleaseMetadata {
708 name: "myapp",
709 version: &version,
710 description: "description goes here 🌈",
711 source_files: &[
712 Utf8PathBuf::from("gleam.toml"),
713 Utf8PathBuf::from("src/thingy.gleam"),
714 Utf8PathBuf::from("src/whatever.gleam"),
715 ],
716 generated_files: &[
717 (Utf8PathBuf::from("src/myapp.app"), "".into()),
718 (Utf8PathBuf::from("src/thingy.erl"), "".into()),
719 (Utf8PathBuf::from("src/whatever.erl"), "".into()),
720 ],
721 licenses: &licences,
722 links: vec![("homepage", homepage), ("github", github)],
723 requirements: vec![
724 ReleaseRequirement {
725 name: "wibble",
726 requirement: &req1,
727 },
728 ReleaseRequirement {
729 name: "wobble",
730 requirement: &req2,
731 },
732 ],
733 build_tools: vec!["gleam", "rebar3"],
734 };
735 assert_eq!(
736 meta.as_erlang(),
737 r#"{<<"name">>, <<"myapp">>}.
738{<<"app">>, <<"myapp">>}.
739{<<"version">>, <<"1.2.3">>}.
740{<<"description">>, <<"description goes here 🌈"/utf8>>}.
741{<<"licenses">>, [<<"MIT">>, <<"MPL-2.0">>]}.
742{<<"build_tools">>, [<<"gleam">>, <<"rebar3">>]}.
743{<<"links">>, [
744 {<<"homepage">>, <<"https://gleam.run/">>},
745 {<<"github">>, <<"https://github.com/lpil/myapp">>}
746]}.
747{<<"requirements">>, [
748 {<<"wibble">>, [
749 {<<"app">>, <<"wibble">>},
750 {<<"optional">>, false},
751 {<<"requirement">>, <<"~> 1.2.3 or >= 5.0.0">>}
752 ]},
753 {<<"wobble">>, [
754 {<<"app">>, <<"wobble">>},
755 {<<"optional">>, false},
756 {<<"requirement">>, <<"~> 1.2">>}
757 ]}
758]}.
759{<<"files">>, [
760 <<"gleam.toml">>,
761 <<"src/myapp.app">>,
762 <<"src/thingy.erl">>,
763 <<"src/thingy.gleam">>,
764 <<"src/whatever.erl">>,
765 <<"src/whatever.gleam">>
766]}.
767"#
768 .to_string()
769 );
770}
771
772#[test]
773fn prevent_publish_local_dependency() {
774 let config = PackageConfig {
775 dependencies: [("provided".into(), Requirement::path("./path/to/package"))].into(),
776 ..Default::default()
777 };
778 assert_eq!(
779 metadata_config(&config, &[], &[]),
780 Err(Error::PublishNonHexDependencies {
781 package: "provided".into()
782 })
783 );
784}
785
786#[test]
787fn prevent_publish_git_dependency() {
788 let config = PackageConfig {
789 dependencies: [(
790 "provided".into(),
791 Requirement::git("https://github.com/gleam-lang/gleam.git", "da6e917"),
792 )]
793 .into(),
794 ..Default::default()
795 };
796 assert_eq!(
797 metadata_config(&config, &[], &[]),
798 Err(Error::PublishNonHexDependencies {
799 package: "provided".into()
800 })
801 );
802}
803
804fn quotes(x: &str) -> String {
805 format!(r#"<<"{x}">>"#)
806}
807
808#[test]
809fn exported_project_files_test() {
810 let tmp = tempfile::tempdir().unwrap();
811 let path = Utf8PathBuf::from_path_buf(tmp.path().join("my_project")).expect("Non Utf8 Path");
812
813 let exported_project_files = &[
814 "LICENCE",
815 "LICENCE.md",
816 "LICENCE.txt",
817 "LICENSE",
818 "LICENSE.md",
819 "LICENSE.txt",
820 "NOTICE",
821 "NOTICE.md",
822 "NOTICE.txt",
823 "README",
824 "README.md",
825 "README.txt",
826 "gleam.toml",
827 "priv/ignored",
828 "priv/wibble",
829 "priv/wobble.js",
830 "src/.hidden/hidden_ffi.erl",
831 "src/.hidden/hidden_ffi.mjs",
832 "src/.hidden_ffi.erl",
833 "src/.hidden_ffi.mjs",
834 "src/exported.gleam",
835 "src/exported_ffi.erl",
836 "src/exported_ffi.ex",
837 "src/exported_ffi.hrl",
838 "src/exported_ffi.js",
839 "src/exported_ffi.mjs",
840 "src/exported_ffi.ts",
841 "src/ignored.gleam",
842 "src/ignored_ffi.erl",
843 "src/ignored_ffi.mjs",
844 "src/nested/exported.gleam",
845 "src/nested/exported_ffi.erl",
846 "src/nested/exported_ffi.ex",
847 "src/nested/exported_ffi.hrl",
848 "src/nested/exported_ffi.js",
849 "src/nested/exported_ffi.mjs",
850 "src/nested/exported_ffi.ts",
851 "src/nested/ignored.gleam",
852 "src/nested/ignored_ffi.erl",
853 "src/nested/ignored_ffi.mjs",
854 ];
855
856 let unexported_project_files = &[
857 ".git/",
858 ".github/workflows/test.yml",
859 ".gitignore",
860 "build/",
861 "ignored.txt",
862 "src/.hidden/hidden.gleam", // Not a valid Gleam module path
863 "src/.hidden.gleam", // Not a valid Gleam module name
864 "src/also-ignored.gleam", // Not a valid Gleam module name
865 "test/exported_test.gleam",
866 "test/exported_test_ffi.erl",
867 "test/exported_test_ffi.ex",
868 "test/exported_test_ffi.hrl",
869 "test/exported_test_ffi.js",
870 "test/exported_test_ffi.mjs",
871 "test/exported_test_ffi.ts",
872 "test/ignored_test.gleam",
873 "test/ignored_test_ffi.erl",
874 "test/ignored_test_ffi.mjs",
875 "test/nested/exported_test.gleam",
876 "test/nested/exported_test_ffi.erl",
877 "test/nested/exported_test_ffi.ex",
878 "test/nested/exported_test_ffi.hrl",
879 "test/nested/exported_test_ffi.js",
880 "test/nested/exported_test_ffi.mjs",
881 "test/nested/exported_test_ffi.ts",
882 "test/nested/ignored.gleam",
883 "test/nested/ignored_test_ffi.erl",
884 "test/nested/ignored_test_ffi.mjs",
885 "dev/exported_test_ffi.erl",
886 "dev/exported_test_ffi.ex",
887 "dev/exported_test_ffi.hrl",
888 "dev/exported_test_ffi.js",
889 "dev/exported_test_ffi.mjs",
890 "dev/exported_test_ffi.ts",
891 "dev/ignored_test.gleam",
892 "dev/ignored_test_ffi.erl",
893 "dev/ignored_test_ffi.mjs",
894 "dev/nested/exported_test.gleam",
895 "dev/nested/exported_test_ffi.erl",
896 "dev/nested/exported_test_ffi.ex",
897 "dev/nested/exported_test_ffi.hrl",
898 "dev/nested/exported_test_ffi.js",
899 "dev/nested/exported_test_ffi.mjs",
900 "dev/nested/exported_test_ffi.ts",
901 "dev/nested/ignored.gleam",
902 "dev/nested/ignored_test_ffi.erl",
903 "dev/nested/ignored_test_ffi.mjs",
904 "unrelated-file.txt",
905 ];
906
907 let gitignore = "ignored*
908src/also-ignored.gleam";
909
910 for &file in exported_project_files
911 .iter()
912 .chain(unexported_project_files)
913 {
914 if file.ends_with("/") {
915 fs::mkdir(path.join(file)).unwrap();
916 continue;
917 }
918
919 let contents = match file {
920 ".gitignore" => gitignore,
921 _ => "",
922 };
923
924 fs::write(&path.join(file), contents).unwrap();
925 }
926
927 let mut chosen_exported_files = project_files(&path).unwrap();
928 chosen_exported_files.sort_unstable();
929
930 let expected_exported_files = exported_project_files
931 .iter()
932 .map(|s| path.join(s))
933 .collect_vec();
934
935 assert_eq!(expected_exported_files, chosen_exported_files);
936}