- 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 def enumerate_dependencies
packages.map do |package_name, sources| packages.map do |package_name, sources|
Licensed::Sources::Manifest::Dependency.new(sources, Licensed::Sources::Manifest::Dependency.new(
@config.root, name: package_name,
package_license(package_name), path: configured_license_path(package_name) || sources_license_path(sources),
{ sources: sources,
metadata: {
"type" => Manifest.type, "type" => Manifest.type,
"name" => package_name, "name" => package_name,
"version" => package_version(sources) "version" => package_version(sources)
@ -32,7 +33,7 @@ module Licensed
end end
# Returns the license path for a package specified in the configuration. # 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) license_path = @config.dig("manifest", "licenses", package_name)
return unless license_path return unless license_path
@ -41,6 +42,25 @@ module Licensed
license_path license_path
end 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 # Returns a map of package names -> array of full source paths found
# in the app manifest # in the app manifest
def packages def packages
@ -169,87 +189,44 @@ module Licensed
) )
/imx.freeze /imx.freeze
def initialize(sources, root, license_path, metadata = {}) def initialize(name:, path:, sources:, metadata: {})
@sources = sources @sources = sources
license_path ||= sources_license_path(sources, root) super(name: name, path: path, metadata: metadata)
super license_path, metadata
end end
# Detects license information and sets it on this dependency object. def project_files
# After calling `detect_license!``, the license is set at files = super
# `dependency["license"]` and legal text is set to `dependency.text` files.concat(source_files) if files.empty?
def detect_license! files
# if no license key is found for the project, try to create a end
# 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
super def source_files
ensure @source_files ||= begin
File.delete(tmp_license_file) if tmp_license_file && File.exist?(tmp_license_file) @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 end
private private
# Returns the top-most directory that is common to all paths in `sources` # Returns the comment text with leading * and whitespace stripped
def sources_license_path(sources, root) def source_comment_text(comment)
# return the source directory if there is only one source given indent = nil
return source_directory(sources[0]) if sources.size == 1 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 line[/([^\w\r\n]{0,#{indent}})(.*)/m, 2]
# the project this is run for should be excluded from the manifest, end.join
# 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
end end
end end
end end

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

@ -1,6 +1,5 @@
test/fixtures/manifest/test_1.c: manifest_test test/fixtures/manifest/test_1.c: manifest_test
test/fixtures/manifest/subfolder/test_2.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.c: bsd3_single_header_license
test/fixtures/manifest/single_license_header/source_2.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 test/fixtures/manifest/multiple_license_headers/source.c: bsd3_multi_header_license

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

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

@ -31,22 +31,10 @@ describe Licensed::Sources::Manifest do
describe "dependencies" do describe "dependencies" do
it "includes dependencies from the manifest" 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 dep
assert_equal "manifest", dep["type"] assert_equal "manifest", dep.data["type"]
assert dep["version"] # version comes from git, just make sure its there assert dep.data["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
end end
it "uses a license specified in the configuration if provided" do 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 assert dep
dep.detect_license! assert_equal "mit", dep.data["license"]
assert_equal "mit", dep["license"]
license_path = File.join(config.root, config.dig("manifest", "licenses", "manifest_test")) 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 end
it "prefers licenses from license files" do 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 assert dep
dep.detect_license! assert_equal "mit", dep.data["license"]
assert_equal "mit", dep["license"] refute_empty dep.data.licenses
refute_nil dep.text
end end
it "detects license from source header comments if license files are not found" do 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 assert dep
dep.detect_license! assert_equal "bsd-3-clause", dep.data["license"]
assert_equal "bsd-3-clause", dep["license"] assert_equal 1, dep.data.licenses.uniq.size
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"))
end end
it "detects unique license content from multiple headers" do 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 assert dep
dep.detect_license! assert_equal "bsd-3-clause", dep.data["license"]
# because there are different licenses/copyrights that need to be included assert_equal 2, dep.data.licenses.uniq.size
# 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)
end end
it "preserves legal notices when detecting license content from comments" do 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 assert dep
dep.detect_license! refute_empty dep.data.notices
refute_nil dep.text
assert dep.text.include?(dep.notices.join("\n#{Licensed::License::TEXT_SEPARATOR}\n").strip)
end end
end end