Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempt conversion of SPDX license expressions to Homebrew DSL #1345

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ url = "2.5.0"
parse-changelog = "0.6.8"
schemars = "0.8.21"
serde_yml = "0.0.10"
spdx = "0.10.6"

[workspace.metadata.release]
shared-version = true
Expand Down
1 change: 1 addition & 0 deletions cargo-dist/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ temp-dir.workspace = true
sha3.workspace = true
blake2.workspace = true
serde_yml.workspace = true
spdx.workspace = true

[dev-dependencies]
homedir.workspace = true
Expand Down
158 changes: 157 additions & 1 deletion cargo-dist/src/backend/installer/homebrew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
use axoasset::LocalAsset;
use cargo_dist_schema::DistManifest;
use serde::Serialize;
use spdx::{
expression::{ExprNode, Operator},
Expression, ParseError,
};

use super::InstallerInfo;
use crate::{
Expand Down Expand Up @@ -178,9 +182,66 @@ pub fn to_class_case(app_name: &str) -> String {
chars.iter().collect()
}

/// Converts SPDX license string into Homebrew Ruby DSL
// Homebrew DSL reference: https://docs.brew.sh/License-Guidelines
pub fn to_homebrew_license_format(app_license: &str) -> Result<String, ParseError> {
let spdx = Expression::parse(app_license)?;
let mut spdx = spdx.iter().peekable();
let mut buffer: Vec<String> = vec![];

while let Some(token) = spdx.next() {
match token {
ExprNode::Req(req) => {
// If token is a license, push to the buffer as-is for next operator or end.
let requirement = format!("\"{}\"", req.req);
buffer.push(requirement);
}
ExprNode::Op(op) => {
// If token is an operation, group operands in buffer into all_of/any_of clause.
// Operations are postfix, so we pop off the previous two elements and combine.
let second_operand = buffer.pop().expect("Operator missing first operand.");
cxreiff marked this conversation as resolved.
Show resolved Hide resolved
let first_operand = buffer.pop().expect("Operator missing second operand.");
let mut combined = format!("{}, {}", first_operand, second_operand);

// If the operations that immediately follow are the same as the current operation,
// squash their operands into the same all_of/any_of clause.
while let Some(ExprNode::Op(next_op)) = spdx.peek() {
if next_op != op {
break;
}
let _ = spdx.next();
let operand = buffer.pop().expect("Operator missing first operand.");
combined = format!("{}, {}", operand, combined);
}

// Use corresponding homebrew DSL keyword and square bracket the list of licenses.
let operation = match op {
Operator::And => "all_of",
Operator::Or => "any_of",
};
let mut enclosed = format!("{operation}: [{combined}]");

// Only wrap all_of/any_of clause in brackets if it is nested within an outer clause.
if spdx.peek().is_some() {
enclosed = format!("{{ {enclosed} }}");
}

// Push clause back onto the buffer, as it might be an operand in another clause.
buffer.push(enclosed);
}
}
}

// After all tokens have been iterated through, if the SPDX expression is well-formed, there
// should only be a single element left in the buffer: a single license or outermost clause.
Ok(buffer[0].clone())
}

#[cfg(test)]
mod tests {
use super::to_class_case;
use spdx::ParseError;

use super::{to_class_case, to_homebrew_license_format};

fn run_comparison(in_str: &str, expected: &str) {
let out_str = to_class_case(in_str);
Expand Down Expand Up @@ -265,4 +326,99 @@ mod tests {
fn ampersand_but_no_digit() {
run_comparison("openssl@blah", "Openssl@blah");
}

fn run_spdx_comparison(spdx_string: &str, homebrew_dsl: &str) {
mistydemeo marked this conversation as resolved.
Show resolved Hide resolved
let result = to_homebrew_license_format(spdx_string).unwrap();
assert_eq!(result, homebrew_dsl);
}

#[test]
fn spdx_single_license() {
run_spdx_comparison("MIT", r#""MIT""#);
}

#[test]
fn spdx_single_license_with_plus() {
run_spdx_comparison("Apache-2.0+", r#""Apache-2.0+""#);
}

#[test]
fn spdx_two_licenses_any() {
run_spdx_comparison("MIT OR 0BSD", r#"any_of: ["MIT", "0BSD"]"#);
}

#[test]
fn spdx_two_licenses_all() {
run_spdx_comparison("MIT AND 0BSD", r#"all_of: ["MIT", "0BSD"]"#);
}

#[test]
fn spdx_two_licenses_with_plus() {
run_spdx_comparison("MIT OR EPL-1.0+", r#"any_of: ["MIT", "EPL-1.0+"]"#);
}

#[test]
fn spdx_three_licenses() {
run_spdx_comparison(
"MIT OR Apache-2.0 OR CC-BY-4.0",
r#"any_of: ["MIT", "Apache-2.0", "CC-BY-4.0"]"#,
);
}

#[test]
fn spdx_three_licenses_or_and() {
run_spdx_comparison(
"MIT OR Apache-2.0 AND CC-BY-4.0",
r#"any_of: ["MIT", { all_of: ["Apache-2.0", "CC-BY-4.0"] }]"#,
mistydemeo marked this conversation as resolved.
Show resolved Hide resolved
);
}

#[test]
fn spdx_three_licenses_and_or() {
run_spdx_comparison(
"MIT AND Apache-2.0 OR CC-BY-4.0",
r#"any_of: [{ all_of: ["MIT", "Apache-2.0"] }, "CC-BY-4.0"]"#,
cxreiff marked this conversation as resolved.
Show resolved Hide resolved
);
}

#[test]
fn spdx_parentheses() {
run_spdx_comparison(
"MIT OR (0BSD AND Zlib) OR curl",
r#"any_of: ["MIT", { all_of: ["0BSD", "Zlib"] }, "curl"]"#,
);
}

#[test]
fn spdx_nested_parentheses() {
run_spdx_comparison(
"MIT AND (Apache-2.0 OR (CC-BY-4.0 AND 0BSD))",
r#"all_of: ["MIT", { any_of: ["Apache-2.0", { all_of: ["CC-BY-4.0", "0BSD"] }] }]"#,
);
}

fn run_malformed_spdx(spdx_string: &str) {
let result = to_homebrew_license_format(spdx_string);
assert!(matches!(result, Err(ParseError { .. })));
}

#[test]
fn spdx_invalid_license_name() {
run_malformed_spdx("foo");
}

#[test]
fn spdx_invalid_just_operator() {
run_malformed_spdx("AND");
}

#[test]
fn spdx_invalid_dangling_operator() {
run_malformed_spdx("MIT OR");
}

#[test]
fn spdx_invalid_adjacent_operator() {
run_malformed_spdx("MIT AND OR Apache-2.0");
}
}
8 changes: 7 additions & 1 deletion cargo-dist/src/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ use tracing::{info, warn};
use crate::announce::{self, AnnouncementTag, TagMode};
use crate::backend::ci::github::GithubCiInfo;
use crate::backend::ci::CiInfo;
use crate::backend::installer::homebrew::to_homebrew_license_format;
use crate::config::{
DependencyKind, DirtyMode, ExtraArtifact, GithubPermissionMap, GithubReleasePhase,
LibraryStyle, ProductionMode, SystemDependencies,
Expand Down Expand Up @@ -2116,6 +2117,11 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
release.app_desc.clone()
};
let app_license = release.app_license.clone();
let homebrew_dsl_license = app_license.as_ref().map(|app_license| {
// Parse SPDX license expression and convert to Homebrew's Ruby license DSL.
// If expression is malformed, fall back to plain input license string.
to_homebrew_license_format(app_license).unwrap_or(format!("\"{app_license}\""))
});
let app_homepage_url = if release.app_homepage_url.is_none() {
warn!("The Homebrew publish job is enabled but no homepage was specified\n consider adding `homepage = ` to package in Cargo.toml");
release.app_repository_url.clone()
Expand Down Expand Up @@ -2161,7 +2167,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
name: app_name,
formula_class: to_class_case(formula),
desc: app_desc,
license: app_license,
license: homebrew_dsl_license,
homepage: app_homepage_url,
tap,
dependencies,
Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/templates/installer/homebrew.rb.j2
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class {{ formula_class }} < Formula
{%- endif %}
{#- #}
{%- if license %}
license "{{ license }}"
license {{ license }}
{%- endif %}
{%- if dependencies|length > 0 %}
{% for dep in dependencies %}
Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_abyss.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://fake.axo.dev/faker/axolotlsay/fake-id-do-not-upload/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_abyss_only.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://fake.axo.dev/faker/axolotlsay/fake-id-do-not-upload/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_alias.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {"axolotlsay": ["axolotlsay-link"]}, "x86_64-apple-darwin": {"axolotlsay": ["axolotlsay-link"]}, "x86_64-pc-windows-gnu": {"axolotlsay.exe": ["axolotlsay-link.exe"]}, "x86_64-unknown-linux-gnu": {"axolotlsay": ["axolotlsay-link"]}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {"nosuchbin": ["axolotlsay-link1", "axolotlsay-link2"]}, "x86_64-apple-darwin": {"nosuchbin": ["axolotlsay-link1", "axolotlsay-link2"]}, "x86_64-pc-windows-gnu": {"nosuchbin.exe": ["axolotlsay-link1.exe", "axolotlsay-link2.exe"]}, "x86_64-unknown-linux-gnu": {"nosuchbin": ["axolotlsay-link1", "axolotlsay-link2"]}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_basic.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_basic_lies.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,7 @@ class Axolotlsay < Formula
sha256 "CENSORED"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_custom_formula.snap
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class AxolotlBrew < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2794,7 +2794,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay-hybrid/releases/download/v0.10.2/axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_several_aliases.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {"axolotlsay": ["axolotlsay-link1", "axolotlsay-link2"]}, "x86_64-apple-darwin": {"axolotlsay": ["axolotlsay-link1", "axolotlsay-link2"]}, "x86_64-pc-windows-gnu": {"axolotlsay.exe": ["axolotlsay-link1.exe", "axolotlsay-link2.exe"]}, "x86_64-unknown-linux-gnu": {"axolotlsay": ["axolotlsay-link1", "axolotlsay-link2"]}}

Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/tests/snapshots/axolotlsay_updaters.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ class Axolotlsay < Formula
url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.2/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
end
end
license "MIT OR Apache-2.0"
license any_of: ["MIT", "Apache-2.0"]

BINARY_ALIASES = {"aarch64-apple-darwin": {}, "x86_64-apple-darwin": {}, "x86_64-pc-windows-gnu": {}, "x86_64-unknown-linux-gnu": {}}

Expand Down
Loading
Loading