Skip to content

Commit ff50464

Browse files
nodegTest User
authored andcommitted
feat: Add license and SPDX identifier
Fixes - #12 - #6 Signed-off-by: Dominik Gedon <dominik.gedon@suse.com>
1 parent 50edb03 commit ff50464

9 files changed

Lines changed: 330 additions & 7 deletions

File tree

lib/template/package.spec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ cp -a <%= output %> %{buildroot}%{_javadir}/<%= File.basename(output) %>
6666

6767
%files
6868
%defattr(-,root,root)
69+
<% license_files.each do |file| %>
70+
%doc <%= file %>
71+
<% end %>
6972
%{_javadir}/*
7073

7174
%changelog

lib/tetra.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# third party libraries
2121
require "clamp"
2222
require "text"
23+
require "yaml"
2324
require "zip"
2425

2526
# base components
@@ -48,6 +49,7 @@
4849
require "tetra/pom_getter"
4950

5051
# package building
52+
require "tetra/license_mapper"
5153
require "tetra/packages/speccable"
5254
require "tetra/packages/scriptable"
5355
require "tetra/packages/kit_package"

lib/tetra/constants.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
module Tetra
55
CCOLLECTIONS = "commons-collections4-4.5.0-M2-src".freeze
66
CHUNK_SEPARATOR_VERSION_MATCHER = /[.\-_ ~,]/ # Constant regex for splitting version chunks
7+
LICENSE_MAP_PATH = File.join(__dir__, "data", "license_map.yml").freeze
78
end

lib/tetra/data/license_map.yml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Mappings of Raw Maven License Names -> SPDX Identifiers
2+
# Based on https://spdx.org/licenses/ and openSUSE/Fedora standards
3+
4+
# --- Apache Licenses ---
5+
"The Apache Software License, Version 2.0": "Apache-2.0"
6+
"Apache License, Version 2.0": "Apache-2.0"
7+
"Apache License Version 2.0": "Apache-2.0"
8+
"Apache 2.0": "Apache-2.0"
9+
"Apache License 2.0": "Apache-2.0"
10+
"AL 2.0": "Apache-2.0"
11+
"Apache Software License - Version 2.0": "Apache-2.0"
12+
"The Apache License, Version 2.0": "Apache-2.0"
13+
"Apache-2.0": "Apache-2.0"
14+
15+
"Apache License, Version 1.1": "Apache-1.1"
16+
"The Apache Software License, Version 1.1": "Apache-1.1"
17+
"Apache 1.1": "Apache-1.1"
18+
19+
"Apache License, Version 1.0": "Apache-1.0"
20+
21+
# --- MIT Licenses ---
22+
"The MIT License": "MIT"
23+
"MIT License": "MIT"
24+
"MIT": "MIT"
25+
"MIT license": "MIT"
26+
"The MIT License (MIT)": "MIT"
27+
"Common Development and Distribution License (CDDL) + GPLv2 with classpath exception": "MIT" # Often mislabeled in older POMs
28+
29+
# --- Eclipse Public Licenses ---
30+
"Eclipse Public License 1.0": "EPL-1.0"
31+
"Eclipse Public License - v 1.0": "EPL-1.0"
32+
"Eclipse Public License, Version 1.0": "EPL-1.0"
33+
"EPL 1.0": "EPL-1.0"
34+
35+
"Eclipse Public License 2.0": "EPL-2.0"
36+
"Eclipse Public License - v 2.0": "EPL-2.0"
37+
"EPL 2.0": "EPL-2.0"
38+
39+
# --- GPL (General Public License) ---
40+
"GNU General Public License, version 2": "GPL-2.0-only"
41+
"GNU General Public License, Version 2": "GPL-2.0-only"
42+
"GPL 2.0": "GPL-2.0-only"
43+
"GPLv2": "GPL-2.0-only"
44+
"The GNU General Public License, Version 2": "GPL-2.0-only"
45+
46+
"GNU General Public License, version 2 (GPLv2)": "GPL-2.0-only"
47+
"GNU General Public License, version 3": "GPL-3.0-only"
48+
"GPL 3.0": "GPL-3.0-only"
49+
"GPLv3": "GPL-3.0-only"
50+
51+
# --- LGPL (Lesser General Public License) ---
52+
"GNU Lesser General Public License": "LGPL-2.1-only"
53+
"GNU Lesser General Public License, version 2.1": "LGPL-2.1-only"
54+
"LGPL 2.1": "LGPL-2.1-only"
55+
"LGPLv2.1": "LGPL-2.1-only"
56+
"The GNU Lesser General Public License, Version 2.1": "LGPL-2.1-only"
57+
58+
"GNU Lesser General Public License, version 3": "LGPL-3.0-only"
59+
"LGPL 3.0": "LGPL-3.0-only"
60+
"LGPLv3": "LGPL-3.0-only"
61+
62+
# --- BSD Licenses ---
63+
"The BSD License": "BSD-2-Clause" # Ambiguous, but often 2-clause in modern Java
64+
"BSD License": "BSD-2-Clause"
65+
"BSD-2-Clause": "BSD-2-Clause"
66+
"Two-clause BSD license": "BSD-2-Clause"
67+
"The BSD 2-Clause License": "BSD-2-Clause"
68+
69+
"New BSD License": "BSD-3-Clause"
70+
"BSD 3-Clause": "BSD-3-Clause"
71+
"BSD 3-Clause License": "BSD-3-Clause"
72+
"The BSD 3-Clause License": "BSD-3-Clause"
73+
"Revised BSD License": "BSD-3-Clause"
74+
"Modified BSD License": "BSD-3-Clause"
75+
76+
"BSD 0-Clause": "0BSD"
77+
"0BSD": "0BSD"
78+
79+
# --- CDDL (Common Development and Distribution License) ---
80+
"Common Development and Distribution License": "CDDL-1.0"
81+
"CDDL 1.0": "CDDL-1.0"
82+
"CDDL-1.0": "CDDL-1.0"
83+
"COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0": "CDDL-1.0"
84+
85+
"Common Development and Distribution License 1.1": "CDDL-1.1"
86+
"CDDL 1.1": "CDDL-1.1"
87+
88+
# --- MPL (Mozilla Public License) ---
89+
"Mozilla Public License Version 1.1": "MPL-1.1"
90+
"MPL 1.1": "MPL-1.1"
91+
92+
"Mozilla Public License Version 2.0": "MPL-2.0"
93+
"MPL 2.0": "MPL-2.0"
94+
"The Mozilla Public License, Version 2.0": "MPL-2.0"
95+
96+
# --- ISC License ---
97+
"ISC License": "ISC"
98+
"The ISC License": "ISC"
99+
100+
# --- Creative Commons ---
101+
"Creative Commons Zero v1.0 Universal": "CC0-1.0"
102+
"CC0 1.0 Universal": "CC0-1.0"
103+
"Public Domain": "CC0-1.0" # Approximation for Public Domain in SPDX
104+
105+
"Creative Commons Attribution 3.0": "CC-BY-3.0"
106+
"CC BY 3.0": "CC-BY-3.0"
107+
108+
"Creative Commons Attribution 4.0": "CC-BY-4.0"
109+
"CC BY 4.0": "CC-BY-4.0"
110+
111+
# --- Other Common Java Licenses ---
112+
"The JSON License": "JSON"
113+
"JSON": "JSON"
114+
115+
"Unlicense": "Unlicense"
116+
"The Unlicense": "Unlicense"
117+
118+
"WTFPL": "WTFPL"
119+
"Do What The F*ck You Want To Public License": "WTFPL"
120+
121+
"Bouncy Castle Licence": "MIT"
122+
"PostgreSQL License": "PostgreSQL"
123+
"The PostgreSQL License": "PostgreSQL"
124+
125+
"Zlib License": "Zlib"
126+
"The zlib/libpng License": "Zlib"

lib/tetra/license_mapper.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module Tetra
4+
# Handles mapping raw license names to SPDX identifiers
5+
class LicenseMapper
6+
def self.map(raw_name)
7+
return "unknown" if raw_name.nil?
8+
9+
@mapping ||= YAML.load_file(Tetra::LICENSE_MAP_PATH)
10+
11+
# Strip whitespace
12+
key = raw_name.strip
13+
14+
# Return the mapped SPDX ID, or the original name if no match found
15+
@mapping[key] || key
16+
end
17+
end
18+
end

lib/tetra/packages/package.rb

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require "forwardable"
4-
53
module Tetra
64
# represents a Java project packaged in tetra
75
class Package
@@ -15,7 +13,6 @@ class Package
1513
def_delegator :@project, :src_archive
1614
def_delegator :@kit, :name, :kit_name
1715
def_delegator :@kit, :version, :kit_version
18-
def_delegator :@pom, :license_name, :license
1916
def_delegator :@pom, :url
2017
def_delegator :@pom, :group_id
2118
def_delegator :@pom, :version
@@ -29,6 +26,27 @@ def initialize(project, pom_path = nil, filter = nil, patches = [])
2926
@patches = patches.map { |f| File.basename(f) }
3027
end
3128

29+
def license
30+
Tetra::LicenseMapper.map(@pom.license_name)
31+
end
32+
33+
# Scans for license files in the src/ directory (Issue #12)
34+
def license_files
35+
@project.from_directory do
36+
# Tetra unpacks sources into 'src/', so we search there.
37+
# We perform a case-insensitive search for common license file names.
38+
patterns = ["src/LICENSE*", "src/COPYING*", "src/COPYRIGHT*"]
39+
40+
files = Dir.glob(patterns, File::FNM_CASEFOLD).select do |file|
41+
File.file?(file)
42+
end
43+
44+
# Return just the filenames (e.g. "LICENSE") so the spec file
45+
# (which runs inside src/ due to %setup -n src) can reference them directly.
46+
files.map { |f| File.basename(f) }.uniq.sort
47+
end
48+
end
49+
3250
def artifact_ids
3351
@pom.modules.any? ? @pom.modules : [@pom.artifact_id]
3452
end
@@ -51,18 +69,18 @@ def outputs
5169
end
5270

5371
def cleanup_description(raw, max_length)
72+
return "" if raw.nil?
73+
5474
# Normalize spaces (collapse multiple spaces/newlines to single space)
5575
clean = raw.gsub(/\s+/, " ").strip
5676

5777
# Truncate to max_length
5878
clean = clean[0, max_length]
5979

6080
# Remove the last word if it looks cut off (ends in letters, not punctuation)
61-
# Note: This assumes descriptions usually end with punctuation.
6281
clean = clean.sub(/\s\w+\z/, "")
6382

64-
# Remove ALL trailing dots efficiently (Security Fix for ReDoS)
65-
# Replaces clean.sub(/\.+\z/, "")
83+
# Remove ALL trailing dots efficiently
6684
clean = clean.chomp(".") while clean.end_with?(".")
6785

6886
clean
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
describe "tetra generate-spec license handling", type: :aruba do
6+
# Helper to generate a minimal, valid POM with DYNAMIC artifactId
7+
def create_pom(group_id, artifact_id, version, license_name)
8+
<<~XML
9+
<project>
10+
<modelVersion>4.0.0</modelVersion>
11+
<groupId>#{group_id}</groupId>
12+
<artifactId>#{artifact_id}</artifactId>
13+
<version>#{version}</version>
14+
<packaging>jar</packaging>
15+
<licenses>
16+
<license>
17+
<name>#{license_name}</name>
18+
<url>http://example.com/license</url>
19+
</license>
20+
</licenses>
21+
</project>
22+
XML
23+
end
24+
25+
# Raw POM License Name => Expected SPDX Identifier
26+
let(:test_cases) do
27+
{
28+
"The Apache Software License, Version 2.0" => "Apache-2.0",
29+
"The MIT License" => "MIT",
30+
"Eclipse Public License 1.0" => "EPL-1.0",
31+
"GNU General Public License, version 2" => "GPL-2.0-only",
32+
"GNU Lesser General Public License" => "LGPL-2.1-only",
33+
"Unmapped Custom License" => "Unmapped Custom License"
34+
}
35+
end
36+
37+
it "detects license names and converts them to SPDX" do
38+
test_cases.each do |raw_name, expected_spdx|
39+
project_name = "proj-#{expected_spdx.downcase.gsub(/[^a-z0-9]/, "")}"
40+
version = "1.0"
41+
group_id = "com.example"
42+
43+
# Place files directly in the tarball root so they unpack to src/
44+
write_file("pom.xml", create_pom(group_id, project_name, version, raw_name))
45+
write_file("LICENSE", "License text")
46+
write_file("COPYING", "Copying text")
47+
write_file("README", "Readme text")
48+
49+
tarball_name = "#{project_name}.tar"
50+
run_command_and_stop("tar -cvf #{tarball_name} pom.xml LICENSE COPYING README")
51+
52+
run_command_and_stop("tetra init #{project_name} #{tarball_name}")
53+
54+
cd(project_name) do
55+
run_command_and_stop("git config user.email 'test@example.com'")
56+
run_command_and_stop("git config user.name 'Test User'")
57+
58+
run_command("tetra dry-run")
59+
sleep 2 # Wait for shell
60+
61+
# SIMULATE BUILD:
62+
# Create a JAR that matches the POM's artifactId and version.
63+
# This confirms to tetra that the build produced valid output.
64+
jar_name = "#{project_name}-#{version}.jar"
65+
type "echo 'dummy-content' > #{jar_name}"
66+
67+
type "exit"
68+
last_command_started.wait
69+
expect(last_command_started).to be_successfully_executed
70+
71+
# We explicitly point to src/pom.xml so tetra finds the metadata securely.
72+
run_command_and_stop("tetra generate-spec src/pom.xml")
73+
74+
spec_path = "packages/#{project_name}/#{project_name}.spec"
75+
76+
expect(file?(spec_path)).to be(true), "Spec file missing for #{project_name}"
77+
78+
spec_content = read(spec_path).join("\n")
79+
80+
expect(spec_content).to include("Version: #{version}"),
81+
"Metadata missing. Tetra failed to read POM for #{project_name}."
82+
83+
expect(spec_content).to match(/License:\s+#{Regexp.escape(expected_spdx)}/),
84+
"Failed to map '#{raw_name}' to '#{expected_spdx}'"
85+
86+
expect(spec_content).to include("%doc LICENSE")
87+
expect(spec_content).to include("%doc COPYING")
88+
end
89+
end
90+
end
91+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
require "tetra/license_mapper"
5+
6+
describe "Tetra::LicenseMapper" do
7+
describe ".map" do
8+
it "maps known Maven license names to SPDX identifiers" do
9+
expect(Tetra::LicenseMapper.map("The Apache Software License, Version 2.0")).to eq("Apache-2.0")
10+
expect(Tetra::LicenseMapper.map("Eclipse Public License 1.0")).to eq("EPL-1.0")
11+
expect(Tetra::LicenseMapper.map("Mozilla Public License Version 2.0")).to eq("MPL-2.0")
12+
expect(Tetra::LicenseMapper.map("Common Development and Distribution License")).to eq("CDDL-1.0")
13+
expect(Tetra::LicenseMapper.map("The JSON License")).to eq("JSON")
14+
end
15+
16+
it "handles whitespace around the license name" do
17+
expect(Tetra::LicenseMapper.map(" Apache License, Version 2.0 ")).to eq("Apache-2.0")
18+
end
19+
20+
it "returns the original name if no mapping is found" do
21+
expect(Tetra::LicenseMapper.map("My Custom Proprietary License")).to eq("My Custom Proprietary License")
22+
end
23+
24+
it "returns 'unknown' for nil input" do
25+
expect(Tetra::LicenseMapper.map(nil)).to eq("unknown")
26+
end
27+
28+
it "loads the mapping from the correct file path" do
29+
expect(File).to exist(Tetra::LICENSE_MAP_PATH)
30+
end
31+
end
32+
end

0 commit comments

Comments
 (0)