Skip to content
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
3 changes: 0 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ PATH
clamp (~> 1.3.2)
erb (~> 6.0)
json_pure (>= 2.6.3, < 2.9.0)
open4 (~> 1.3.4)
rexml (>= 3.2.9, < 3.5.0)
rubyzip (~> 3.2)
text (~> 1.3.1)
Expand Down Expand Up @@ -72,7 +71,6 @@ GEM
memoist3 (1.0.0)
mini_mime (1.1.5)
multi_test (1.1.0)
open4 (1.3.4)
parallel (1.27.0)
parser (3.3.10.1)
ast (~> 2.4.1)
Expand Down Expand Up @@ -203,7 +201,6 @@ CHECKSUMS
memoist3 (1.0.0) sha256=686e42402cf150a362050c23143dc57b0ef88f8c344943ff8b7845792b50d56f
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
multi_test (1.1.0) sha256=e9e550cdd863fb72becfe344aefdcd4cbd26ebf307847f4a6c039a4082324d10
open4 (1.3.4) sha256=a1df037310624ecc1ea1d81264b11c83e96d0c3c1c6043108d37d396dcd0f4b1
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
prism (1.8.0) sha256=84453a16ef5530ea62c5f03ec16b52a459575ad4e7b9c2b360fd8ce2c39c1254
Expand Down
3 changes: 3 additions & 0 deletions lib/template/package.spec
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ cp -a <%= output %> %{buildroot}%{_javadir}/<%= File.basename(output) %>

%files
%defattr(-,root,root)
<% license_files.each do |file| %>
%doc <%= file %>
<% end %>
%{_javadir}/*

%changelog
2 changes: 2 additions & 0 deletions lib/tetra.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# third party libraries
require "clamp"
require "text"
require "yaml"
require "zip"

# base components
Expand Down Expand Up @@ -48,6 +49,7 @@
require "tetra/pom_getter"

# package building
require "tetra/license_mapper"
require "tetra/packages/speccable"
require "tetra/packages/scriptable"
require "tetra/packages/kit_package"
Expand Down
8 changes: 8 additions & 0 deletions lib/tetra/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@
module Tetra
CCOLLECTIONS = "commons-collections4-4.5.0-M2-src".freeze
CHUNK_SEPARATOR_VERSION_MATCHER = /[.\-_ ~,]/ # Constant regex for splitting version chunks
LICENSE_MAP_PATH = File.join(__dir__, "data", "license_map.yml").freeze
LICENSE_MAPPINGS = {
"The Apache Software License, Version 2.0" => "Apache-2.0",
"The MIT License" => "MIT",
"Eclipse Public License 1.0" => "EPL-1.0",
"GNU General Public License, version 2" => "GPL-2.0-only",
"GNU Lesser General Public License" => "LGPL-2.1-only"
}.freeze
end
124 changes: 124 additions & 0 deletions lib/tetra/data/license_map.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Mappings of Raw Maven License Names -> SPDX Identifiers
# Based on https://spdx.org/licenses/ and openSUSE/Fedora standards

# --- Apache Licenses ---
"The Apache Software License, Version 2.0": "Apache-2.0"
"Apache License, Version 2.0": "Apache-2.0"
"Apache License Version 2.0": "Apache-2.0"
"Apache 2.0": "Apache-2.0"
"Apache License 2.0": "Apache-2.0"
"AL 2.0": "Apache-2.0"
"Apache Software License - Version 2.0": "Apache-2.0"
"The Apache License, Version 2.0": "Apache-2.0"
"Apache-2.0": "Apache-2.0"

"Apache License, Version 1.1": "Apache-1.1"
"The Apache Software License, Version 1.1": "Apache-1.1"
"Apache 1.1": "Apache-1.1"

"Apache License, Version 1.0": "Apache-1.0"

# --- MIT Licenses ---
"The MIT License": "MIT"
"MIT License": "MIT"
"MIT": "MIT"
"MIT license": "MIT"
"The MIT License (MIT)": "MIT"

# --- Eclipse Public Licenses ---
"Eclipse Public License 1.0": "EPL-1.0"
"Eclipse Public License - v 1.0": "EPL-1.0"
"Eclipse Public License, Version 1.0": "EPL-1.0"
"EPL 1.0": "EPL-1.0"

"Eclipse Public License 2.0": "EPL-2.0"
"Eclipse Public License - v 2.0": "EPL-2.0"
"EPL 2.0": "EPL-2.0"

# --- GPL (General Public License) ---
"GNU General Public License, version 2": "GPL-2.0-only"
"GNU General Public License, Version 2": "GPL-2.0-only"
"GPL 2.0": "GPL-2.0-only"
"GPLv2": "GPL-2.0-only"
"The GNU General Public License, Version 2": "GPL-2.0-only"

"GNU General Public License, version 2 (GPLv2)": "GPL-2.0-only"
"GNU General Public License, version 3": "GPL-3.0-only"
"GPL 3.0": "GPL-3.0-only"
"GPLv3": "GPL-3.0-only"

# --- LGPL (Lesser General Public License) ---
"GNU Lesser General Public License": "LGPL-2.1-only"
"GNU Lesser General Public License, version 2.1": "LGPL-2.1-only"
"LGPL 2.1": "LGPL-2.1-only"
"LGPLv2.1": "LGPL-2.1-only"
"The GNU Lesser General Public License, Version 2.1": "LGPL-2.1-only"

"GNU Lesser General Public License, version 3": "LGPL-3.0-only"
"LGPL 3.0": "LGPL-3.0-only"
"LGPLv3": "LGPL-3.0-only"

# --- BSD Licenses ---
"The BSD License": "BSD-2-Clause" # Ambiguous, but often 2-clause in modern Java
"BSD License": "BSD-2-Clause"
"BSD-2-Clause": "BSD-2-Clause"
"Two-clause BSD license": "BSD-2-Clause"
"The BSD 2-Clause License": "BSD-2-Clause"

"New BSD License": "BSD-3-Clause"
"BSD 3-Clause": "BSD-3-Clause"
"BSD 3-Clause License": "BSD-3-Clause"
"The BSD 3-Clause License": "BSD-3-Clause"
"Revised BSD License": "BSD-3-Clause"
"Modified BSD License": "BSD-3-Clause"

"BSD 0-Clause": "0BSD"
"0BSD": "0BSD"

# --- CDDL (Common Development and Distribution License) ---
"Common Development and Distribution License": "CDDL-1.0"
"CDDL 1.0": "CDDL-1.0"
"CDDL-1.0": "CDDL-1.0"
"COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0": "CDDL-1.0"

"Common Development and Distribution License 1.1": "CDDL-1.1"
"CDDL 1.1": "CDDL-1.1"

# --- MPL (Mozilla Public License) ---
"Mozilla Public License Version 1.1": "MPL-1.1"
"MPL 1.1": "MPL-1.1"

"Mozilla Public License Version 2.0": "MPL-2.0"
"MPL 2.0": "MPL-2.0"
"The Mozilla Public License, Version 2.0": "MPL-2.0"

# --- ISC License ---
"ISC License": "ISC"
"The ISC License": "ISC"

# --- Creative Commons ---
"Creative Commons Zero v1.0 Universal": "CC0-1.0"
"CC0 1.0 Universal": "CC0-1.0"
"Public Domain": "CC0-1.0" # Approximation for Public Domain in SPDX

"Creative Commons Attribution 3.0": "CC-BY-3.0"
"CC BY 3.0": "CC-BY-3.0"

"Creative Commons Attribution 4.0": "CC-BY-4.0"
"CC BY 4.0": "CC-BY-4.0"

# --- Other Common Java Licenses ---
"The JSON License": "JSON"
"JSON": "JSON"

"Unlicense": "Unlicense"
"The Unlicense": "Unlicense"

"WTFPL": "WTFPL"
"Do What The F*ck You Want To Public License": "WTFPL"

"PostgreSQL License": "PostgreSQL"
"The PostgreSQL License": "PostgreSQL"

"Zlib License": "Zlib"
"The zlib/libpng License": "Zlib"
34 changes: 34 additions & 0 deletions lib/tetra/license_mapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Tetra
# Handles mapping raw license names to SPDX identifiers
class LicenseMapper
class << self
def map(raw_name)
return "unknown" if raw_name.nil? || raw_name.strip.empty?

name = raw_name.strip
mapping[name] || name
end

def reset!
@mapping = nil
end

private

def mapping
@mapping ||= begin
load_mapping
rescue Errno::ENOENT, Psych::SyntaxError, Psych::DisallowedClass, IOError => e
warn "Warning: failed to load license mapping from #{Tetra::LICENSE_MAP_PATH}: #{e.class}: #{e.message}"
{}
end
end

def load_mapping
YAML.safe_load_file(Tetra::LICENSE_MAP_PATH) || {}
end
end
end
end
40 changes: 34 additions & 6 deletions lib/tetra/packages/package.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

require "forwardable"

module Tetra
# represents a Java project packaged in tetra
class Package
Expand All @@ -15,7 +13,6 @@ class Package
def_delegator :@project, :src_archive
def_delegator :@kit, :name, :kit_name
def_delegator :@kit, :version, :kit_version
def_delegator :@pom, :license_name, :license
def_delegator :@pom, :url
def_delegator :@pom, :group_id
def_delegator :@pom, :version
Expand All @@ -29,6 +26,37 @@ def initialize(project, pom_path = nil, filter = nil, patches = [])
@patches = patches.map { |f| File.basename(f) }
end

def license
Tetra::LicenseMapper.map(@pom.license_name)
end

# Scans for license files in the src/ directory (Issue #12)
def license_files
@project.from_directory do
# Tetra unpacks sources into 'src/', so we search there.
# We manually check filenames with Ruby regex to ensure reliable
# case-insensitive matching across different OS filesystems (Linux vs macOS).

candidates = Dir.glob("src/*")

files = candidates.select do |file|
next unless File.file?(file)

basename = File.basename(file)

# Match standard license filenames (case insensitive)
# Equivalent to: LICENSE*, COPYING*, COPYRIGHT*
basename.match?(/^LICENSE/i) ||
basename.match?(/^COPYING/i) ||
basename.match?(/^COPYRIGHT/i)
end

# Return just the filenames (e.g. "LICENSE") so the spec file
# (which runs inside src/ due to %setup -n src) can reference them directly.
files.map { |f| File.basename(f) }.uniq.sort
end
end

def artifact_ids
@pom.modules.any? ? @pom.modules : [@pom.artifact_id]
end
Expand All @@ -51,18 +79,18 @@ def outputs
end

def cleanup_description(raw, max_length)
return "" if raw.nil? || raw.strip.empty?

# Normalize spaces (collapse multiple spaces/newlines to single space)
clean = raw.gsub(/\s+/, " ").strip

# Truncate to max_length
clean = clean[0, max_length]

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

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

clean
Expand Down
74 changes: 74 additions & 0 deletions spec/lib/coarse/license_detection_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require "spec_helper"

describe "tetra generate-spec license handling", type: :aruba do
def create_pom(group_id, artifact_id, version, license_name)
<<~XML
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>#{group_id}</groupId>
<artifactId>#{artifact_id}</artifactId>
<version>#{version}</version>
<packaging>jar</packaging>
<licenses>
<license>
<name>#{license_name}</name>
<url>http://example.com/license</url>
</license>
</licenses>
</project>
XML
end

Tetra::LICENSE_MAPPINGS.each do |raw_name, expected_spdx|
it "correctly maps '#{raw_name}' to '#{expected_spdx}'" do
project_name = "proj-#{expected_spdx.downcase.gsub(/[^a-z0-9]/, "")}"
version = "1.0"
group_id = "com.example"

write_file("pom.xml", create_pom(group_id, project_name, version, raw_name))
write_file("LICENSE", "License text")
write_file("COPYING", "Copying text")
write_file("README", "Readme text")

tarball_name = "#{project_name}.tar"
run_command_and_stop("tar -cvf #{tarball_name} pom.xml LICENSE COPYING README")

run_command_and_stop("tetra init #{project_name} #{tarball_name}")

cd(project_name) do
# NOTE: Identity is already set in spec_helper's around hook,
# but we can keep these for explicit project-level config
run_command_and_stop("git config user.email 'test@example.com'")
run_command_and_stop("git config user.name 'Test User'")

# --- NON-INTERACTIVE DRY-RUN ---
run_command("tetra dry-run")
last_command_started.write <<~COMMANDS
touch #{project_name}-#{version}.jar
echo 'changes' >> README
exit 0
COMMANDS

last_command_started.stop
expect(last_command_started).to be_successfully_executed

run_command_and_stop("tetra generate-spec src/pom.xml")

possible_paths = [
"packages/#{project_name}/package.spec",
"packages/#{project_name}/#{project_name}.spec"
]
spec_path = possible_paths.find { |path| file?(path) }

expect(spec_path).not_to be_nil, "Spec file missing. Checked: #{possible_paths}"

spec_content = read(spec_path).join("\n")
expect(spec_content).to match(/License:\s+#{Regexp.escape(expected_spdx)}/)
expect(spec_content).to include("%doc LICENSE")
expect(spec_content).to include("%doc COPYING")
end
end
end
end
Loading