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

Configure Feed

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

gleam / compiler-cli / src / publish.rs
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}