diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e39dc45137..4e0bdb4a188 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -474,7 +474,7 @@ jobs: run: make test working-directory: ./test/hextarball - - name: Test running modules + - name: test/running_modules run: make test-all working-directory: ./test/running_modules @@ -482,7 +482,7 @@ jobs: run: ./test.sh working-directory: ./test/multi_namespace - - name: Test multi namespace bug + - name: test/multi_namespace_not_top_level run: ./test.sh working-directory: ./test/multi_namespace_not_top_level diff --git a/CHANGELOG.md b/CHANGELOG.md index db04eb99410..7534e6cace5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ ([Louis Pilfold](https://github.com/lpil)) - The build tool now emits errors for unknown fields in package config, - except ones under `[tools]`. For example, with this: + except for the ones under `[tools]`. For example, with this: ```toml name = "hello" @@ -58,7 +58,7 @@ my-awesome-setting = true ``` - Building the project will emit error. + Building the project will emit an error. ([Andrey Kozhev](https://github.com/ankddev)) @@ -70,6 +70,14 @@ arguments by taking into account, whether it's a function or a constructor. ([Andrey Kozhev](https://github.com/ankddev)) +- The build tool will now refuse to publish any package that has the default + README generated by the `gleam new` command. + ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) + +- The build tool will now refuse to publish any package that has no README, or + an empty README. + ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) + ### Language server - The language server now allows extracting the start of a pipeline into a diff --git a/compiler-cli/src/new.rs b/compiler-cli/src/new.rs index 75abf7c24b0..bbdeeec7a18 100644 --- a/compiler-cli/src/new.rs +++ b/compiler-cli/src/new.rs @@ -84,34 +84,7 @@ impl FileToCreate { }; match self { - Self::Readme => Some(format!( - r#"# {project_name} - -[![Package Version](https://img.shields.io/hexpm/v/{project_name})](https://hex.pm/packages/{project_name}) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/{project_name}/) - -```sh -gleam add {project_name}@1 -``` -```gleam -import {project_name} - -pub fn main() -> Nil {{ - // TODO: An example of the project in use -}} -``` - -Further documentation can be found at . - -## Development - -```sh -gleam run # Run the project -gleam test # Run the tests -``` -"#, - )), - + Self::Readme => Some(default_readme(project_name)), Self::Gitignore if !skip_git => Some( "*.beam *.ez @@ -202,6 +175,36 @@ jobs: } } +pub fn default_readme(project_name: &str) -> String { + format!( + r#"# {project_name} + +[![Package Version](https://img.shields.io/hexpm/v/{project_name})](https://hex.pm/packages/{project_name}) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/{project_name}/) + +```sh +gleam add {project_name}@1 +``` +```gleam +import {project_name} + +pub fn main() -> Nil {{ + // TODO: An example of the project in use +}} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` +"#, + ) +} + impl Creator { fn new(options: NewOptions, gleam_version: &'static str) -> Result { Self::new_with_confirmation(options, gleam_version, crate::cli::confirm) diff --git a/compiler-cli/src/publish.rs b/compiler-cli/src/publish.rs index ff8d4eae054..f70bed8c5d8 100644 --- a/compiler-cli/src/publish.rs +++ b/compiler-cli/src/publish.rs @@ -8,7 +8,7 @@ use gleam_core::{ build::{Codegen, Compile, Mode, Options, Package, Target}, config::{GleamVersion, PackageConfig, SpdxLicense}, docs::{Dependency, DependencyKind, DocContext}, - error::{SmallVersion, wrap}, + error::{InvalidReadmeReason, SmallVersion, wrap}, hex, manifest::ManifestPackageSource, paths::{self, ProjectPaths}, @@ -20,7 +20,7 @@ use itertools::Itertools; use sha2::Digest; use std::{collections::HashMap, io::Write, path::PathBuf, time::Instant}; -use crate::{build, cli, docs, fs, http::HttpClient}; +use crate::{build, cli, docs, fs, http::HttpClient, new::default_readme}; const CORE_TEAM_PUBLISH_PASSWORD: &str = "Trans rights are human rights"; @@ -31,6 +31,8 @@ pub fn command(paths: &ProjectPaths, replace: bool, i_am_sure: bool) -> Result<( && check_for_version_zero(&config)? && check_repo_url(&config, i_am_sure)?; + check_for_invalid_readme(&config, paths)?; + if !should_publish { println!("Not publishing."); return Ok(()); @@ -130,6 +132,45 @@ HTML documentation will work: Ok(()) } +fn check_for_invalid_readme(config: &PackageConfig, paths: &ProjectPaths) -> Result<(), Error> { + let normalise = |string: String| { + string + .trim() + .replace("\r\n", "") + .replace("\n", "") + .replace("\t", "") + .replace(" ", "") + }; + + let project_readme = match fs::read(paths.readme()) { + Err(Error::FileIo { + err: Some(message), .. + }) if message.contains("No such file or directory") => { + return Err(Error::CannotPublishWithInvalidReadme { + reason: InvalidReadmeReason::Missing, + }); + } + Err(error) => return Err(error), + Ok(project_readme) => project_readme, + }; + + let normalised_project_readme = normalise(project_readme); + if normalised_project_readme.is_empty() { + return Err(Error::CannotPublishWithInvalidReadme { + reason: InvalidReadmeReason::Empty, + }); + } + + let default_readme = default_readme(config.name.as_str()); + if normalised_project_readme == normalise(default_readme) { + return Err(Error::CannotPublishWithInvalidReadme { + reason: InvalidReadmeReason::Default, + }); + } + + Ok(()) +} + fn check_for_name_squatting(package: &Package) -> Result<(), Error> { if package.modules.len() > 1 { return Ok(()); diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs index 1a751875c66..61d97bc840b 100644 --- a/compiler-core/src/error.rs +++ b/compiler-core/src/error.rs @@ -312,6 +312,9 @@ file_names.iter().map(|x| x.as_str()).join(", "))] #[error("The modules {unfinished:?} are empty and so cannot be published")] CannotPublishEmptyModules { unfinished: Vec }, + #[error("Publishing packages with an invalid README is not permitted")] + CannotPublishWithInvalidReadme { reason: InvalidReadmeReason }, + #[error("Publishing packages to reserve names is not permitted")] HexPackageSquatting, @@ -346,6 +349,13 @@ file_names.iter().map(|x| x.as_str()).join(", "))] CannotAddSelfAsDependency { name: EcoString }, } +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum InvalidReadmeReason { + Missing, + Empty, + Default, +} + // A wrapper that ignores the inner value for equality: #[derive(Debug, Clone)] pub struct NeverEqual(pub T); @@ -862,6 +872,63 @@ package deletion or account suspension. }] } + Error::CannotPublishWithInvalidReadme { + reason: InvalidReadmeReason::Default, + } => { + let text = + "You appear to be attempting to publish a package with the default README +generated by the `gleam new` command. That is meant as a placeholder and a +published package should have its own carefully written README. +" + .into(); + + vec![Diagnostic { + title: "Cannot publish with default README".into(), + text, + level: Level::Error, + location: None, + hint: Some( + "Update your project's README to describe it before publishing".into(), + ), + }] + } + + Error::CannotPublishWithInvalidReadme { + reason: InvalidReadmeReason::Missing, + } => { + let text = "You appear to be attempting to publish a package with no README. +All published packages should have one. +" + .into(); + + vec![Diagnostic { + title: "Cannot publish with no README".into(), + text, + level: Level::Error, + location: None, + hint: Some("Add a README to your project before publishing.".into()), + }] + } + + Error::CannotPublishWithInvalidReadme { + reason: InvalidReadmeReason::Empty, + } => { + let text = "You appear to be attempting to publish a package with an empty README. +All published packages should have a non empty README. +" + .into(); + + vec![Diagnostic { + title: "Cannot publish with empty README".into(), + text, + level: Level::Error, + location: None, + hint: Some( + "Update your project's README to describe it before publishing".into(), + ), + }] + } + Error::CannotPublishWithDefaultMain { package_name } => { let text = wrap_format!( "Packages with the default main function cannot be published diff --git a/test/multi_namespace/README.md b/test/multi_namespace/README.md new file mode 100644 index 00000000000..622f79bfc53 --- /dev/null +++ b/test/multi_namespace/README.md @@ -0,0 +1,3 @@ +# Multi namespace project + +should not be published! diff --git a/test/multi_namespace_not_top_level/README.md b/test/multi_namespace_not_top_level/README.md new file mode 100644 index 00000000000..622f79bfc53 --- /dev/null +++ b/test/multi_namespace_not_top_level/README.md @@ -0,0 +1,3 @@ +# Multi namespace project + +should not be published! diff --git a/test/publishing_default_readme/.gitignore b/test/publishing_default_readme/.gitignore new file mode 100644 index 00000000000..c1dc9b0dd00 --- /dev/null +++ b/test/publishing_default_readme/.gitignore @@ -0,0 +1,3 @@ +*.beam +*.ez +build diff --git a/test/publishing_default_readme/README.md b/test/publishing_default_readme/README.md new file mode 100644 index 00000000000..bf7a3b0c07e --- /dev/null +++ b/test/publishing_default_readme/README.md @@ -0,0 +1,24 @@ +# default_readme + +[![Package Version](https://img.shields.io/hexpm/v/default_readme)](https://hex.pm/packages/default_readme) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/default_readme/) + +```sh +gleam add default_readme@1 +``` +```gleam +import default_readme + +pub fn main() -> Nil { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/test/publishing_default_readme/gleam.toml b/test/publishing_default_readme/gleam.toml new file mode 100644 index 00000000000..76042ba249a --- /dev/null +++ b/test/publishing_default_readme/gleam.toml @@ -0,0 +1,7 @@ +name = "default_readme" +version = "1.0.0" +description = "Test project for default readme" +licences = ["Apache-2.0"] + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" diff --git a/test/publishing_default_readme/manifest.toml b/test/publishing_default_readme/manifest.toml new file mode 100644 index 00000000000..88377adc600 --- /dev/null +++ b/test/publishing_default_readme/manifest.toml @@ -0,0 +1,9 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.62.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "0080706D3A5A9A36C40C68481D1D231D243AF602E6D2A2BE67BA8F8F4DFF45EC" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } diff --git a/test/publishing_default_readme/src/default_readme.gleam b/test/publishing_default_readme/src/default_readme.gleam new file mode 100644 index 00000000000..85abef09193 --- /dev/null +++ b/test/publishing_default_readme/src/default_readme.gleam @@ -0,0 +1,21 @@ +import gleam/io + +/// This tests that a project with a default readme doesn't get published. +/// +pub fn main() -> Nil { + greeting() + first_line() + second_line() +} + +fn greeting() { + io.println("Hello from default_readme!") +} + +fn first_line() { + io.println("Here we have some additional code so that this is not mistaken") +} + +fn second_line() { + io.println("for a default main project, that would be rejected as well!") +} diff --git a/test/publishing_default_readme/test.sh b/test/publishing_default_readme/test.sh new file mode 100755 index 00000000000..d534ee4793e --- /dev/null +++ b/test/publishing_default_readme/test.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -eu + +GLEAM_COMMAND=${GLEAM_COMMAND:-"cargo run --quiet --"} + +g() { + echo "Running: $GLEAM_COMMAND $@" + $GLEAM_COMMAND "$@" +} + +echo Resetting the build directory to get to a known state +rm -fr build + +echo Running publish should not publish anything +if yes "n" | g publish; then + echo "Expected publish to fail, but it succeeded" + exit 1 +fi + +echo +echo Success! 💖 +echo diff --git a/test/publishing_empty_readme/.gitignore b/test/publishing_empty_readme/.gitignore new file mode 100644 index 00000000000..c1dc9b0dd00 --- /dev/null +++ b/test/publishing_empty_readme/.gitignore @@ -0,0 +1,3 @@ +*.beam +*.ez +build diff --git a/test/publishing_empty_readme/README.md b/test/publishing_empty_readme/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/publishing_empty_readme/gleam.toml b/test/publishing_empty_readme/gleam.toml new file mode 100644 index 00000000000..d97b24244b8 --- /dev/null +++ b/test/publishing_empty_readme/gleam.toml @@ -0,0 +1,7 @@ +name = "empty_readme" +version = "1.0.0" +description = "Test project for empty readme" +licences = ["Apache-2.0"] + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" diff --git a/test/publishing_empty_readme/manifest.toml b/test/publishing_empty_readme/manifest.toml new file mode 100644 index 00000000000..88377adc600 --- /dev/null +++ b/test/publishing_empty_readme/manifest.toml @@ -0,0 +1,9 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.62.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "0080706D3A5A9A36C40C68481D1D231D243AF602E6D2A2BE67BA8F8F4DFF45EC" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } diff --git a/test/publishing_empty_readme/src/empty_readme.gleam b/test/publishing_empty_readme/src/empty_readme.gleam new file mode 100644 index 00000000000..d2318ee3ee0 --- /dev/null +++ b/test/publishing_empty_readme/src/empty_readme.gleam @@ -0,0 +1,21 @@ +import gleam/io + +/// This tests that a project with an empty readme doesn't get published. +/// +pub fn main() -> Nil { + greeting() + first_line() + second_line() +} + +fn greeting() { + io.println("Hello from empty_readme!") +} + +fn first_line() { + io.println("Here we have some additional code so that this is not mistaken") +} + +fn second_line() { + io.println("for a default main project, that would be rejected as well!") +} diff --git a/test/publishing_empty_readme/test.sh b/test/publishing_empty_readme/test.sh new file mode 100755 index 00000000000..d534ee4793e --- /dev/null +++ b/test/publishing_empty_readme/test.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -eu + +GLEAM_COMMAND=${GLEAM_COMMAND:-"cargo run --quiet --"} + +g() { + echo "Running: $GLEAM_COMMAND $@" + $GLEAM_COMMAND "$@" +} + +echo Resetting the build directory to get to a known state +rm -fr build + +echo Running publish should not publish anything +if yes "n" | g publish; then + echo "Expected publish to fail, but it succeeded" + exit 1 +fi + +echo +echo Success! 💖 +echo diff --git a/test/publishing_no_readme/.gitignore b/test/publishing_no_readme/.gitignore new file mode 100644 index 00000000000..c1dc9b0dd00 --- /dev/null +++ b/test/publishing_no_readme/.gitignore @@ -0,0 +1,3 @@ +*.beam +*.ez +build diff --git a/test/publishing_no_readme/gleam.toml b/test/publishing_no_readme/gleam.toml new file mode 100644 index 00000000000..4e33df4fe8a --- /dev/null +++ b/test/publishing_no_readme/gleam.toml @@ -0,0 +1,7 @@ +name = "no_readme" +version = "1.0.0" +description = "Test project for no readme" +licences = ["Apache-2.0"] + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" diff --git a/test/publishing_no_readme/manifest.toml b/test/publishing_no_readme/manifest.toml new file mode 100644 index 00000000000..88377adc600 --- /dev/null +++ b/test/publishing_no_readme/manifest.toml @@ -0,0 +1,9 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.62.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "0080706D3A5A9A36C40C68481D1D231D243AF602E6D2A2BE67BA8F8F4DFF45EC" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } diff --git a/test/publishing_no_readme/src/no_readme.gleam b/test/publishing_no_readme/src/no_readme.gleam new file mode 100644 index 00000000000..65db7252dc3 --- /dev/null +++ b/test/publishing_no_readme/src/no_readme.gleam @@ -0,0 +1,21 @@ +import gleam/io + +/// This tests that a project with no readme doesn't get published. +/// +pub fn main() -> Nil { + greeting() + first_line() + second_line() +} + +fn greeting() { + io.println("Hello from no_readme!") +} + +fn first_line() { + io.println("Here we have some additional code so that this is not mistaken") +} + +fn second_line() { + io.println("for a default main project, that would be rejected as well!") +} diff --git a/test/publishing_no_readme/test.sh b/test/publishing_no_readme/test.sh new file mode 100755 index 00000000000..d534ee4793e --- /dev/null +++ b/test/publishing_no_readme/test.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -eu + +GLEAM_COMMAND=${GLEAM_COMMAND:-"cargo run --quiet --"} + +g() { + echo "Running: $GLEAM_COMMAND $@" + $GLEAM_COMMAND "$@" +} + +echo Resetting the build directory to get to a known state +rm -fr build + +echo Running publish should not publish anything +if yes "n" | g publish; then + echo "Expected publish to fail, but it succeeded" + exit 1 +fi + +echo +echo Success! 💖 +echo