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

Add configured license amendment content #624

Merged
merged 3 commits into from
Feb 8, 2023
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
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ reviewed:
- classlist # public domain
- octicons

# Specify additional license terms that have been obtained from a dependency's owner
# which apply to the dependency's license
additional_terms:
bundler:
bcrypt-ruby:
- .licenses/amendments/bundler/bcrypt-ruby/amendment.txt

# A single configuration file can be used to enumerate dependencies for multiple
# projects. Each configuration is referred to as an "application" and must include
# a source path, at a minimum
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
1. [Allowed licenses](./allowed_licenses.md)
1. [Ignoring dependencies](./ignoring_dependencies.md)
1. [Reviewing dependencies](./reviewing_dependencies.md)
1. [Additional license terms](./additional_terms.md)
41 changes: 41 additions & 0 deletions docs/configuration/additional_terms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Additional terms

The `additional_terms` configuration option is used to specify paths to files containing extra licensing terms that do not ship with the dependency package. All files specified are expected to be plain text.

Files containing additional content can be located anywhere on disk that is accessible to licensed. File paths can be specified as a string or array and can contain glob values to simplify configuration inputs. All file paths are evaluated from the [configuration root](./configuration_root.md).

## Examples

**Note** The examples below specify paths to additional files under the `.licenses` folder. This is a logical place to store files containing license terms, but be careful not to store files under paths managed by licensed like `.licenses/<source type>/...`. Running `licensed cache` in the future will delete any files under licensed managed paths that licensed did not create. This is why the below examples use paths like `.licenses/amendments/bundler/...` instead of not `.licenses/bundler/amendments/...`.

### With a string

```yaml
additional_terms:
# specify the type of dependency
bundler:
# specify the dependency name and path to an additional file
<gem-name>: .licenses/amendments/bundler/<gem-name>/terms.txt
```

### With a glob string

```yaml
additional_terms:
# specify the type of dependency
bundler:
# specify the dependency name and one or more additional files with a glob pattern
<gem-name>: .licenses/amendments/bundler/<gem-name>/*.txt
```

### With an array of strings

```yaml
additional_terms:
# specify the type of dependency
bundler:
# specify the dependency name and array of paths to additional files
<gem-name>:
- .licenses/amendments/bundler/<gem-name>/terms-1.txt
- .licenses/amendments/bundler/<gem-name>/terms-2.txt
```
6 changes: 6 additions & 0 deletions lib/licensed/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ def allow(license)
self["allowed"] << license
end

# Returns an array of paths to files containing additional license terms.
def additional_terms_for_dependency(dependency)
amendment_paths = Array(self.dig("additional_terms", dependency["type"], dependency["name"]))
amendment_paths.flat_map { |path| Dir.glob(self.root.join(path)) }
end

private

def any_list_pattern_matched?(list, dependency, match_version: false)
Expand Down
27 changes: 27 additions & 0 deletions lib/licensed/dependency.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Dependency < Licensee::Projects::FSProject
attr_reader :version
attr_reader :errors
attr_reader :path
attr_reader :additional_terms

# Create a new project dependency
#
Expand All @@ -28,6 +29,7 @@ def initialize(name:, version:, path:, search_root: nil, metadata: {}, errors: [
@errors = errors
path = path.to_s
@path = path
@additional_terms = []

# enforcing absolute paths makes life much easier when determining
# an absolute file path in #notices
Expand Down Expand Up @@ -80,6 +82,13 @@ def license_contents
files.compact
end


# Override the behavior of Licensee::Projects::FSProject#project_files to include
# additional license terms
def project_files
super + additional_license_terms_files
end

# Returns legal notices found at the dependency path
def notice_contents
Dir.glob(dir_path.join("*"))
Expand All @@ -102,6 +111,7 @@ def read_file_with_encoding_check(file_path)
def license_content_sources(files)
paths = Array(files).map do |file|
next file[:uri] if file[:uri]
next file[:source] if file[:source]

path = dir_path.join(file[:dir], file[:name])
normalize_source_path(path)
Expand Down Expand Up @@ -157,5 +167,22 @@ def generated_license_contents
"text" => text
}
end

# Returns an array of Licensee::ProjectFiles::LicenseFile created from
# this dependency's additional license terms
def additional_license_terms_files
@additional_license_terms_files ||= begin
files = additional_terms.map do |path|
next unless File.file?(path)

metadata = { dir: File.dirname(path), name: File.basename(path) }
Licensee::ProjectFiles::LicenseFile.new(
load_file(metadata),
{ source: "License terms loaded from #{metadata[:name]}" }
)
end
files.compact
end
end
end
end
9 changes: 8 additions & 1 deletion lib/licensed/sources/source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ def enabled?
# Returns all dependencies that should be evaluated.
# Excludes ignored dependencies.
def dependencies
cached_dependencies.reject { |d| ignored?(d) }
cached_dependencies
.reject { |d| ignored?(d) }
.each { |d| add_additional_terms_from_configuration(d) }
end

# Enumerate all source dependencies. Must be implemented by each source class.
Expand All @@ -88,6 +90,11 @@ def ignored?(dependency)
def cached_dependencies
@dependencies ||= enumerate_dependencies.compact
end

# Add any additional_terms for this dependency that have been added to the configuration
def add_additional_terms_from_configuration(dependency)
dependency.additional_terms.concat config.additional_terms_for_dependency("type" => self.class.type, "name" => dependency.name)
end
end
end
end
52 changes: 52 additions & 0 deletions test/configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -604,4 +604,56 @@
config.cache_path
end
end

describe "additional_terms_for_dependency" do
it "returns an empty array if additional terms aren't configured for a dependency" do
assert_empty config.additional_terms_for_dependency("type" => "test", "name" => "test")
end

it "returns an array of absolute paths to amendment files for a string configuration" do
Dir.mktmpdir do |dir|
Dir.chdir dir do
options["additional_terms"] = { "test" => { "test" => "amendment.txt" } }
File.write "amendment.txt", "amendment"

path = File.join(Dir.pwd, "amendment.txt")
assert_equal [path], config.additional_terms_for_dependency("type" => "test", "name" => "test")
end
end
end

it "returns an array of absolute paths to amendment files for an array configuration" do
Dir.mktmpdir do |dir|
Dir.chdir dir do
options["additional_terms"] = { "test" => { "test" => ["amendment.txt"] } }
File.write "amendment.txt", "amendment"

path = File.join(Dir.pwd, "amendment.txt")
assert_equal [path], config.additional_terms_for_dependency("type" => "test", "name" => "test")
end
end
end

it "strips any amendment paths for files that don't exist" do
options["additional_terms"] = { "test" => { "test" => "amendment.txt" } }
assert_empty config.additional_terms_for_dependency("type" => "test", "name" => "test")
end

it "expands glob patterns" do
Dir.mktmpdir do |dir|
Dir.chdir dir do
Dir.mkdir "amendments"

options["additional_terms"] = { "test" => { "test" => "amendments/*" } }
paths = 2.times.map do |index|
path = "amendments/amendment-#{index}.txt"
File.write path, "amendment-#{index}"
File.join(Dir.pwd, path)
end

assert_equal paths, config.additional_terms_for_dependency("type" => "test", "name" => "test")
end
end
end
end
end
19 changes: 19 additions & 0 deletions test/dependency_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,23 @@ def mkproject(&block)
refute dep.exist?
end
end

describe "project_files" do
it "returns found only project files when the dependency does not contain additional terms" do
mkproject do |dependency|
File.write "LICENSE", Licensee::License.find("mit").text
assert_includes dependency.license_contents,
{ "sources" => "LICENSE", "text" => Licensee::License.find("mit").text }
end
end

it "returns custom license amendment files when the dependency contains additional terms" do
mkproject do |dependency|
File.write "amendment.txt", "license amendment"
dependency.additional_terms << "amendment.txt"
assert_includes dependency.license_contents,
{ "sources" => "License terms loaded from amendment.txt", "text" => "license amendment" }
end
end
end
end
15 changes: 15 additions & 0 deletions test/sources/source_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,20 @@
config.ignore("type" => "test", "name" => "dependency")
assert_empty source.dependencies
end

it "adds the dependency's configured additional terms to dependencies" do
Dir.mktmpdir do |dir|
Dir.chdir dir do
config["additional_terms"] = {
TestSource.type => {
TestSource::DEFAULT_DEPENDENCY_NAME => "amendment.txt"
}
}
File.write "amendment.txt", "amendment"
dep = source.dependencies.first
assert_equal [File.join(Dir.pwd, "amendment.txt")], dep.additional_terms
end
end
end
end
end