- simplifies local dependency class as a Licensee Project
- better license matching on source comments!
This commit is contained in:
Jon Ruskin 2019-01-03 14:10:59 -07:00
Родитель 224e4ecf09
Коммит 3ed3081c1b
4 изменённых файлов: 70 добавлений и 120 удалений

Просмотреть файл

@ -10,10 +10,11 @@ module Licensed
def enumerate_dependencies
packages.map do |package_name, sources|
Licensed::Sources::Manifest::Dependency.new(sources,
@config.root,
package_license(package_name),
{
Licensed::Sources::Manifest::Dependency.new(
name: package_name,
path: configured_license_path(package_name) || sources_license_path(sources),
sources: sources,
metadata: {
"type" => Manifest.type,
"name" => package_name,
"version" => package_version(sources)
@ -32,7 +33,7 @@ module Licensed
end
# Returns the license path for a package specified in the configuration.
def package_license(package_name)
def configured_license_path(package_name)
license_path = @config.dig("manifest", "licenses", package_name)
return unless license_path
@ -41,6 +42,25 @@ module Licensed
license_path
end
# Returns the top-most directory that is common to all paths in `sources`
def sources_license_path(sources)
# if there is more than one source, try to find a directory common to
# all sources
if sources.size > 1
common_prefix = Pathname.common_prefix(*sources).to_path
# don't allow the workspace root to be used as common prefix
# the project this is run for should be excluded from the manifest,
# or ignored in the config. any license in the root should be ignored.
return common_prefix if common_prefix != @config.root
end
# use the first (or only) sources directory to find license information
source = sources.first
return File.dirname(source) if File.file?(source)
source
end
# Returns a map of package names -> array of full source paths found
# in the app manifest
def packages
@ -169,87 +189,44 @@ module Licensed
)
/imx.freeze
def initialize(sources, root, license_path, metadata = {})
def initialize(name:, path:, sources:, metadata: {})
@sources = sources
license_path ||= sources_license_path(sources, root)
super license_path, metadata
super(name: name, path: path, metadata: metadata)
end
# Detects license information and sets it on this dependency object.
# After calling `detect_license!``, the license is set at
# `dependency["license"]` and legal text is set to `dependency.text`
def detect_license!
# if no license key is found for the project, try to create a
# temporary LICENSE file from unique source file license headers
if license_key == "none"
tmp_license_file = write_license_from_source_licenses(self.path, @sources)
reset_license!
end
def project_files
files = super
files.concat(source_files) if files.empty?
files
end
super
ensure
File.delete(tmp_license_file) if tmp_license_file && File.exist?(tmp_license_file)
def source_files
@source_files ||= begin
@sources.select { |f| File.file?(f) }
.map { |f| File.read(f) }
.flat_map { |content| content.scan(HEADER_LICENSE_REGEX) }
.map { |match| match[0] }
.map { |comment| source_comment_text(comment) }
.compact
.map { |content| Licensee::ProjectFiles::LicenseFile.new(content) }
end
end
private
# Returns the top-most directory that is common to all paths in `sources`
def sources_license_path(sources, root)
# return the source directory if there is only one source given
return source_directory(sources[0]) if sources.size == 1
# Returns the comment text with leading * and whitespace stripped
def source_comment_text(comment)
indent = nil
comment.lines.map do |line|
# find the length of the indent as the number of characters
# until the first word character
indent ||= line[/\A([^\w]*)\w/, 1]&.size
common_prefix = Pathname.common_prefix(*sources).to_path
# insert newline for each line until a word character is found
next "\n" unless indent
# don't allow the workspace root to be used as common prefix
# the project this is run for should be excluded from the manifest,
# or ignored in the config. any license in the root should be ignored.
return common_prefix if common_prefix != root
# use the first source directory as the license path.
source_directory(sources.first)
end
# Returns the directory for the source. Checks whether the source
# is a file or a directory
def source_directory(source)
return File.dirname(source) if File.file?(source)
source
end
# Writes any licenses found in source file comments to a LICENSE
# file at `dir`
# Returns the path to the license file
def write_license_from_source_licenses(dir, sources)
license_path = File.join(dir, "LICENSE")
File.open(license_path, "w") do |f|
licenses = source_comment_licenses(sources).uniq
f.puts(licenses.join("\n#{LICENSE_SEPARATOR}\n"))
end
license_path
end
# Returns a list of unique licenses parsed from source comments
def source_comment_licenses(sources)
comments = sources.select { |s| File.file?(s) }.flat_map do |source|
content = File.read(source)
content.scan(HEADER_LICENSE_REGEX).map { |match| match[0] }
end
comments.map do |comment|
# strip leading "*" and whitespace
indent = nil
comment.lines.map do |line|
# find the length of the indent as the number of characters
# until the first word character
indent ||= line[/\A([^\w]*)\w/, 1]&.size
# insert newline for each line until a word character is found
next "\n" unless indent
line[/([^\w\r\n]{0,#{indent}})(.*)/m, 2]
end.join
end
line[/([^\w\r\n]{0,#{indent}})(.*)/m, 2]
end.join
end
end
end

1
test/fixtures/manifest/manifest.yml поставляемый
Просмотреть файл

@ -1,6 +1,5 @@
test/fixtures/manifest/test_1.c: manifest_test
test/fixtures/manifest/subfolder/test_2.c: manifest_test
script/console: other
test/fixtures/manifest/single_license_header/source.c: bsd3_single_header_license
test/fixtures/manifest/single_license_header/source_2.c: bsd3_single_header_license
test/fixtures/manifest/multiple_license_headers/source.c: bsd3_multi_header_license

Просмотреть файл

Просмотреть файл

@ -31,22 +31,10 @@ describe Licensed::Sources::Manifest do
describe "dependencies" do
it "includes dependencies from the manifest" do
dep = source.dependencies.detect { |d| d["name"] == "manifest_test" }
dep = source.dependencies.detect { |d| d.name == "manifest_test" }
assert dep
assert_equal "manifest", dep["type"]
assert dep["version"] # version comes from git, just make sure its there
end
describe "paths" do
it "finds the common folder path for the dependency" do
dep = source.dependencies.detect { |d| d["name"] == "manifest_test" }
assert_equal fixtures, dep.path
end
it "uses the first source folder if there is no common path" do
dep = source.dependencies.detect { |d| d["name"] == "other" }
assert dep.path.end_with?("script")
end
assert_equal "manifest", dep.data["type"]
assert dep.data["version"] # version comes from git, just make sure its there
end
it "uses a license specified in the configuration if provided" do
@ -56,53 +44,39 @@ describe Licensed::Sources::Manifest do
}
}
dep = source.dependencies.detect { |d| d["name"] == "manifest_test" }
dep = source.dependencies.detect { |d| d.name == "manifest_test" }
assert dep
dep.detect_license!
assert_equal "mit", dep["license"]
assert_equal "mit", dep.data["license"]
license_path = File.join(config.root, config.dig("manifest", "licenses", "manifest_test"))
assert_equal File.read(license_path).strip, dep.text
assert_includes dep.data.licenses, File.read(license_path)
end
it "prefers licenses from license files" do
dep = source.dependencies.detect { |d| d["name"] == "mit_license_file" }
dep = source.dependencies.detect { |d| d.name == "mit_license_file" }
assert dep
dep.detect_license!
assert_equal "mit", dep["license"]
refute_nil dep.text
assert_equal "mit", dep.data["license"]
refute_empty dep.data.licenses
end
it "detects license from source header comments if license files are not found" do
dep = source.dependencies.detect { |d| d["name"] == "bsd3_single_header_license" }
dep = source.dependencies.detect { |d| d.name == "bsd3_single_header_license" }
assert dep
dep.detect_license!
assert_equal "bsd-3-clause", dep["license"]
refute_nil dep.text
refute dep.text.include?(Licensed::License::LICENSE_SEPARATOR)
# verify that the license file was removed after evaluation
refute File.exist?(File.join(dep.path, "LICENSE"))
assert_equal "bsd-3-clause", dep.data["license"]
assert_equal 1, dep.data.licenses.uniq.size
end
it "detects unique license content from multiple headers" do
dep = source.dependencies.detect { |d| d["name"] == "bsd3_multi_header_license" }
dep = source.dependencies.detect { |d| d.name == "bsd3_multi_header_license" }
assert dep
dep.detect_license!
# because there are different licenses/copyrights that need to be included
# we aren't able to specify that the actual license content is equivalent
# so we are left with "other"
assert_equal "other", dep["license"]
refute_nil dep.text
assert dep.text.include?(Licensed::License::LICENSE_SEPARATOR)
assert_equal "bsd-3-clause", dep.data["license"]
assert_equal 2, dep.data.licenses.uniq.size
end
it "preserves legal notices when detecting license content from comments" do
dep = source.dependencies.detect { |d| d["name"] == "notices" }
dep = source.dependencies.detect { |d| d.name == "notices" }
assert dep
dep.detect_license!
refute_nil dep.text
assert dep.text.include?(dep.notices.join("\n#{Licensed::License::TEXT_SEPARATOR}\n").strip)
refute_empty dep.data.notices
end
end