licensed 4.0.4 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4862d2463055e895e3aac366c32821ee047360646946860d0cd6eb86ef9701f
4
- data.tar.gz: b75cf3b200090387ef2f104295bb90fc8874668bd895797335f02a23c9357296
3
+ metadata.gz: 2b65e198a420b03486b2680a6a83fb04b4e67684d28bc4ba9ef00c466ffd7489
4
+ data.tar.gz: ab2b10c6e854d3f1d7faa918e48addb497032838fd1cea942ff823053b891150
5
5
  SHA512:
6
- metadata.gz: 766f5196aea9b41034c0a76e11ebe7e7272ce1554661d552ad88895dd45cc43544cb737f7c36173ab612fc71f352d14ac30d2449f7414f8b7813174e294828c5
7
- data.tar.gz: 0c6a31ae81db6abe8744d4a64b07e695b86b18536ce7308f7757c0c69183684e6d39d91082c87edd5a693617f30884a6c322ec3aa0e017a87bc137bf1e0793b7
6
+ metadata.gz: 8dee38c45e73cb03b7c94a9260bb9bc6f5919f53156b69a751238693920e2f120a3dcb0d43f66270fa9abc8305705fdced290978e51bfe762be5f0e5ba00230d
7
+ data.tar.gz: d352b46e40f545f0bd3e1aa29e3bb62454e2e329c64ce9197ba1f9c38b4f11bcbbbecc789658dcff0e6b79e43b4ee9cc839b5476e23cab44a559a605ddbd77b6
data/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## 4.2.0
10
+
11
+ ### Added
12
+
13
+ - Reviewed and ignored configuration lists support matching on versions and version ranges (https://github.com/github/licensed/pull/629)
14
+
15
+ ### Fixed
16
+
17
+ - Licensed should more reliably source dependencies from Gradle >= 8.0 (https://github.com/github/licensed/pull/630)
18
+
19
+ ## 4.1.0
20
+
21
+ ### Added
22
+
23
+ - Custom license terms can be added to dependencies via new configuration options (https://github.com/github/licensed/pull/624)
24
+ - Licensed is now integrated with pnpm to enumerate dependencies (https://github.com/github/licensed/pull/626)
25
+
9
26
  ## 4.0.4
10
27
 
11
28
  ### Changed
@@ -706,4 +723,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
706
723
 
707
724
  Initial release :tada:
708
725
 
709
- [Unreleased]: https://github.com/github/licensed/compare/4.0.4...HEAD
726
+ [Unreleased]: https://github.com/github/licensed/compare/4.2.0...HEAD
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- licensed (4.0.4)
4
+ licensed (4.2.0)
5
5
  json (~> 2.6)
6
6
  licensee (~> 9.16)
7
7
  parallel (~> 1.22)
@@ -9,3 +9,4 @@
9
9
  1. [Allowed licenses](./allowed_licenses.md)
10
10
  1. [Ignoring dependencies](./ignoring_dependencies.md)
11
11
  1. [Reviewing dependencies](./reviewing_dependencies.md)
12
+ 1. [Additional license terms](./additional_terms.md)
@@ -0,0 +1,41 @@
1
+ # Additional terms
2
+
3
+ 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.
4
+
5
+ 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).
6
+
7
+ ## Examples
8
+
9
+ **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/...`.
10
+
11
+ ### With a string
12
+
13
+ ```yaml
14
+ additional_terms:
15
+ # specify the type of dependency
16
+ bundler:
17
+ # specify the dependency name and path to an additional file
18
+ <gem-name>: .licenses/amendments/bundler/<gem-name>/terms.txt
19
+ ```
20
+
21
+ ### With a glob string
22
+
23
+ ```yaml
24
+ additional_terms:
25
+ # specify the type of dependency
26
+ bundler:
27
+ # specify the dependency name and one or more additional files with a glob pattern
28
+ <gem-name>: .licenses/amendments/bundler/<gem-name>/*.txt
29
+ ```
30
+
31
+ ### With an array of strings
32
+
33
+ ```yaml
34
+ additional_terms:
35
+ # specify the type of dependency
36
+ bundler:
37
+ # specify the dependency name and array of paths to additional files
38
+ <gem-name>:
39
+ - .licenses/amendments/bundler/<gem-name>/terms-1.txt
40
+ - .licenses/amendments/bundler/<gem-name>/terms-2.txt
41
+ ```
@@ -17,3 +17,16 @@ ignored:
17
17
  go:
18
18
  - github.com/me/my-repo/**/*
19
19
  ```
20
+
21
+ ## Ignoring dependencies at specific versions
22
+
23
+ Ignore a dependency at specific versions by appending `@<version>` to the end of the dependency's name in an `ignore` list. If a dependency is configured to be ignored at a specific version, licensed will not ignore non-matching versions of the dependency.
24
+
25
+ The version value can be one of:
26
+
27
+ 1. `"*"` - match any version value
28
+ 1. any version string, or version range string, that can be parsed by `Gem::Requirement`
29
+ - a semantic version - `dependency@1.2.3`
30
+ - a gem requirement range - `dependency@~> 1.0.0` or `dependency@< 3.0`
31
+ - see the [Rubygems version guides](https://guides.rubygems.org/patterns/#pessimistic-version-constraint) for more details about specifying gem version requirements
32
+ 1. a value that can't be parsed by `Gem::Requirement`, which will only match dependencies with the same version string
@@ -16,3 +16,16 @@ reviewed:
16
16
  bundler:
17
17
  - gem-using-unallowed-license
18
18
  ```
19
+
20
+ ## Reviewing dependencies at specific versions
21
+
22
+ Review a dependency at specific versions by appending `@<version>` to the end of the dependency's name in an `reviewed` list. If a dependency is configured to be reviewed at a specific version, licensed will not recognize non-matching versions of the dependency as being manually reviewed and accepted.
23
+
24
+ The version value can be one of:
25
+
26
+ 1. `"*"` - match any version value
27
+ 1. any version string, or version range string, that can be parsed by `Gem::Requirement`
28
+ - a semantic version - `dependency@1.2.3`
29
+ - a gem requirement range - `dependency@~> 1.0.0` or `dependency@< 3.0`
30
+ - see the [Rubygems version guides](https://guides.rubygems.org/patterns/#pessimistic-version-constraint) for more details about specifying gem version requirements
31
+ 1. a value that can't be parsed by `Gem::Requirement`, which will only match dependencies with the same version string
@@ -67,6 +67,13 @@ reviewed:
67
67
  - classlist # public domain
68
68
  - octicons
69
69
 
70
+ # Specify additional license terms that have been obtained from a dependency's owner
71
+ # which apply to the dependency's license
72
+ additional_terms:
73
+ bundler:
74
+ bcrypt-ruby:
75
+ - .licenses/amendments/bundler/bcrypt-ruby/amendment.txt
76
+
70
77
  # A single configuration file can be used to enumerate dependencies for multiple
71
78
  # projects. Each configuration is referred to as an "application" and must include
72
79
  # a source path, at a minimum
@@ -0,0 +1,20 @@
1
+ # pnpm
2
+
3
+ The npm source will detect dependencies when `pnpm-lock.yaml` is found at an apps `source_path`. It uses `pnpm licenses list` to enumerate dependencies and metadata.
4
+
5
+ All dependencies enumerated by the pnpm source include the dependency version in the dependency's name identifier. All [reviewed](../configuration/reviewing_dependencies.md) or [ignored](../configuration/ignoring_dependencies.md) dependencies must include a version signifier in the configured dependency name.
6
+
7
+ **NOTE** [pnpm licenses list](https://pnpm.io/cli/licenses) is an experimental CLI command and subject to change. If changes to pnpm result in unexpected or broken behavior in licensed please open an [issue](https://github.com/github/licensed/issues/new).
8
+
9
+ ## Including development dependencies
10
+
11
+ By default, the npm source will exclude all development dependencies. To include development or test dependencies, set `production_only: false` in the licensed configuration.
12
+
13
+ ```yml
14
+ pnpm:
15
+ production_only: false
16
+ ```
17
+
18
+ ## Using licensed with pnpm workspaces
19
+
20
+ Licensed will locate all dependencies from all pnpm workspaces and cannot enumerate dependencies from individual project workspaces. This is a limitation from the pnpm CLI.
@@ -83,7 +83,7 @@ module Licensed
83
83
  report["version"] = dependency.version
84
84
 
85
85
  if save_dependency_record?(dependency, cached_record)
86
- update_dependency_from_cached_record(app, dependency, cached_record)
86
+ update_dependency_from_cached_record(app, source, dependency, cached_record)
87
87
 
88
88
  dependency.record.save(filename)
89
89
  report["cached"] = true
@@ -123,14 +123,14 @@ module Licensed
123
123
  # Update dependency metadata from the cached record, to support:
124
124
  # 1. continuity between cache runs to cut down on churn
125
125
  # 2. notifying users when changed content needs to be reviewed
126
- def update_dependency_from_cached_record(app, dependency, cached_record)
126
+ def update_dependency_from_cached_record(app, source, dependency, cached_record)
127
127
  return if cached_record.nil?
128
128
  return if options[:force]
129
129
 
130
130
  if dependency.record.matches?(cached_record)
131
131
  # use the cached license value if the license text wasn't updated
132
132
  dependency.record["license"] = cached_record["license"]
133
- elsif app.reviewed?(dependency.record)
133
+ elsif app.reviewed?(dependency.record, require_version: source.class.require_matched_dependency_version)
134
134
  # if the license text changed and the dependency is set as reviewed
135
135
  # force a re-review of the dependency
136
136
  dependency.record["review_changed_license"] = true
@@ -60,7 +60,7 @@ module Licensed
60
60
  report.errors << "missing license text" if record.licenses.empty?
61
61
  if record["review_changed_license"]
62
62
  report.errors << "license text has changed and needs re-review. if the new text is ok, remove the `review_changed_license` flag from the cached record"
63
- elsif license_needs_review?(app, record)
63
+ elsif license_needs_review?(app, source, record)
64
64
  report.errors << needs_review_error_message(app, record)
65
65
  end
66
66
  end
@@ -70,9 +70,10 @@ module Licensed
70
70
 
71
71
  # Returns true if a cached record needs further review based on the
72
72
  # record's license(s) and the app's configuration
73
- def license_needs_review?(app, record)
73
+ def license_needs_review?(app, source, record)
74
74
  # review is not needed if the record is set as reviewed
75
- return false if app.reviewed?(record, match_version: data_source == "configuration")
75
+ require_version = data_source == "configuration" || source.class.require_matched_dependency_version
76
+ return false if app.reviewed?(record, require_version: require_version)
76
77
 
77
78
  # review is not needed if the top level license is allowed
78
79
  return false if app.allowed?(record["license"])
@@ -99,7 +100,7 @@ module Licensed
99
100
  error = "dependency needs review"
100
101
 
101
102
  # look for an unversioned reviewed list match
102
- if app.reviewed?(record, match_version: false)
103
+ if app.reviewed?(record, require_version: false)
103
104
  error += ", unversioned 'reviewed' match found: #{record["name"]}"
104
105
  end
105
106
 
@@ -10,6 +10,8 @@ module Licensed
10
10
 
11
11
  DEFAULT_CACHE_PATH = ".licenses".freeze
12
12
 
13
+ ANY_VERSION_REQUIREMENT = "*".freeze
14
+
13
15
  # Returns the root for a configuration in following order of precedence:
14
16
  # 1. explicitly configured "root" property
15
17
  # 2. a found git repository root
@@ -73,8 +75,8 @@ module Licensed
73
75
  end
74
76
 
75
77
  # Is the given dependency reviewed?
76
- def reviewed?(dependency, match_version: false)
77
- any_list_pattern_matched? self["reviewed"][dependency["type"]], dependency, match_version: match_version
78
+ def reviewed?(dependency, require_version: false)
79
+ any_list_pattern_matched? self["reviewed"][dependency["type"]], dependency, require_version: require_version
78
80
  end
79
81
 
80
82
  # Find all reviewed dependencies that match the provided dependency's name
@@ -83,8 +85,8 @@ module Licensed
83
85
  end
84
86
 
85
87
  # Is the given dependency ignored?
86
- def ignored?(dependency)
87
- any_list_pattern_matched? self["ignored"][dependency["type"]], dependency
88
+ def ignored?(dependency, require_version: false)
89
+ any_list_pattern_matched? self["ignored"][dependency["type"]], dependency, require_version: require_version
88
90
  end
89
91
 
90
92
  # Is the license of the dependency allowed?
@@ -93,8 +95,10 @@ module Licensed
93
95
  end
94
96
 
95
97
  # Ignore a dependency
96
- def ignore(dependency)
97
- (self["ignored"][dependency["type"]] ||= []) << dependency["name"]
98
+ def ignore(dependency, at_version: false)
99
+ id = dependency["name"]
100
+ id += "@#{dependency["version"]}" if at_version && dependency["version"]
101
+ (self["ignored"][dependency["type"]] ||= []) << id
98
102
  end
99
103
 
100
104
  # Set a dependency as reviewed
@@ -109,17 +113,49 @@ module Licensed
109
113
  self["allowed"] << license
110
114
  end
111
115
 
116
+ # Returns an array of paths to files containing additional license terms.
117
+ def additional_terms_for_dependency(dependency)
118
+ amendment_paths = Array(self.dig("additional_terms", dependency["type"], dependency["name"]))
119
+ amendment_paths.flat_map { |path| Dir.glob(self.root.join(path)) }
120
+ end
121
+
112
122
  private
113
123
 
114
- def any_list_pattern_matched?(list, dependency, match_version: false)
124
+ def any_list_pattern_matched?(list, dependency, require_version: false)
115
125
  Array(list).any? do |pattern|
116
- if match_version
117
- at_version = "@#{dependency["version"]}"
118
- pattern, pattern_version = pattern.rpartition(at_version).values_at(0, 1)
119
- next false if pattern == "" || pattern_version == ""
126
+ # parse a name and version requirement value from the pattern
127
+ name, requirement = pattern.rpartition("@").values_at(0, 2).map(&:strip)
128
+
129
+ if name == ""
130
+ # if name == "", then the pattern doesn't contain a valid version value.
131
+ # treat the entire pattern as the dependency name with no version.
132
+ name = pattern
133
+ requirement = nil
134
+ elsif !requirement.to_s.empty?
135
+ # check if the version requirement is a valid Gem::Requirement
136
+ # for range matching
137
+ requirements = requirement.split(",").map(&:strip)
138
+ if requirements.all? { |r| Gem::Requirement::PATTERN.match?(r) }
139
+ requirement = Gem::Requirement.new(requirements)
140
+ end
120
141
  end
121
142
 
122
- File.fnmatch?(pattern, dependency["name"], File::FNM_PATHNAME | File::FNM_CASEFOLD)
143
+ # the pattern's name must match the dependency's name
144
+ next false unless File.fnmatch?(name, dependency["name"], File::FNM_PATHNAME | File::FNM_CASEFOLD)
145
+
146
+ # if there is no version requirement configured or if the dependency doesn't have a version
147
+ # specified, return a value based on whether a version match is required
148
+ next !require_version if requirement.nil? || dependency["version"].to_s.empty?
149
+
150
+ case requirement
151
+ when String
152
+ # string match the requirement against "*" or the dependency's version
153
+ [ANY_VERSION_REQUIREMENT, dependency["version"]].any? { |r| requirement == r }
154
+ when Gem::Requirement
155
+ # if the version was parsed as a gem requirement, check whether the version requirement
156
+ # matches the dependency's version
157
+ Gem::Version.correct?(dependency["version"]) && requirement.satisfied_by?(Gem::Version.new(dependency["version"]))
158
+ end
123
159
  end
124
160
  end
125
161
 
@@ -9,6 +9,7 @@ module Licensed
9
9
  attr_reader :version
10
10
  attr_reader :errors
11
11
  attr_reader :path
12
+ attr_reader :additional_terms
12
13
 
13
14
  # Create a new project dependency
14
15
  #
@@ -28,6 +29,7 @@ module Licensed
28
29
  @errors = errors
29
30
  path = path.to_s
30
31
  @path = path
32
+ @additional_terms = []
31
33
 
32
34
  # enforcing absolute paths makes life much easier when determining
33
35
  # an absolute file path in #notices
@@ -80,6 +82,13 @@ module Licensed
80
82
  files.compact
81
83
  end
82
84
 
85
+
86
+ # Override the behavior of Licensee::Projects::FSProject#project_files to include
87
+ # additional license terms
88
+ def project_files
89
+ super + additional_license_terms_files
90
+ end
91
+
83
92
  # Returns legal notices found at the dependency path
84
93
  def notice_contents
85
94
  Dir.glob(dir_path.join("*"))
@@ -90,6 +99,17 @@ module Licensed
90
99
  .select { |notice| notice["text"].length > 0 } # files with content only
91
100
  end
92
101
 
102
+ # Returns a hash of basic metadata about the dependency - name, version, type, etc
103
+ def metadata
104
+ {
105
+ # can be overriden by values in @metadata
106
+ "name" => name,
107
+ "version" => version
108
+ }.merge(
109
+ @metadata
110
+ )
111
+ end
112
+
93
113
  private
94
114
 
95
115
  def read_file_with_encoding_check(file_path)
@@ -102,6 +122,7 @@ module Licensed
102
122
  def license_content_sources(files)
103
123
  paths = Array(files).map do |file|
104
124
  next file[:uri] if file[:uri]
125
+ next file[:source] if file[:source]
105
126
 
106
127
  path = dir_path.join(file[:dir], file[:name])
107
128
  normalize_source_path(path)
@@ -125,14 +146,8 @@ module Licensed
125
146
  # Returns the metadata that represents this dependency. This metadata
126
147
  # is written to YAML in the dependencys cached text file
127
148
  def license_metadata
128
- {
129
- # can be overriden by values in @metadata
130
- "name" => name,
131
- "version" => version
132
- }.merge(
133
- @metadata
134
- ).merge({
135
- # overrides all other values
149
+ metadata.merge({
150
+ # overrides all metadata values
136
151
  "license" => license_key
137
152
  })
138
153
  end
@@ -157,5 +172,22 @@ module Licensed
157
172
  "text" => text
158
173
  }
159
174
  end
175
+
176
+ # Returns an array of Licensee::ProjectFiles::LicenseFile created from
177
+ # this dependency's additional license terms
178
+ def additional_license_terms_files
179
+ @additional_license_terms_files ||= begin
180
+ files = additional_terms.map do |path|
181
+ next unless File.file?(path)
182
+
183
+ metadata = { dir: File.dirname(path), name: File.basename(path) }
184
+ Licensee::ProjectFiles::LicenseFile.new(
185
+ load_file(metadata),
186
+ { source: "License terms loaded from #{metadata[:name]}" }
187
+ )
188
+ end
189
+ files.compact
190
+ end
191
+ end
160
192
  end
161
193
  end
@@ -9,7 +9,7 @@ require "fileutils"
9
9
  module Licensed
10
10
  module Sources
11
11
  class Gradle < Source
12
- DEFAULT_CONFIGURATIONS = ["runtime", "runtimeClasspath"].freeze
12
+ DEFAULT_CONFIGURATIONS = ["runtimeOnly", "runtimeClasspath"].freeze
13
13
  GRADLE_LICENSES_PATH = ".gradle-licenses".freeze
14
14
  GRADLE_LICENSES_CSV_NAME = "licenses.csv".freeze
15
15
  class Dependency < Licensed::Dependency
@@ -46,7 +46,7 @@ module Licensed
46
46
  end
47
47
 
48
48
  def enumerate_dependencies
49
- JSON.parse(gradle_runner.run("printDependencies", config.source_path)).map do |package|
49
+ JSON.parse(gradle_runner.run("printDependencies")).map do |package|
50
50
  name = "#{package['group']}:#{package['name']}"
51
51
  Dependency.new(
52
52
  name: name,
@@ -73,7 +73,7 @@ module Licensed
73
73
  end
74
74
 
75
75
  def gradle_runner
76
- @gradle_runner ||= Runner.new(config.pwd, configurations, executable)
76
+ @gradle_runner ||= Runner.new(configurations, executable)
77
77
  end
78
78
 
79
79
  # Returns the configurations to include in license generation.
@@ -113,7 +113,7 @@ module Licensed
113
113
  begin
114
114
  # create the CSV file including dependency license urls using the gradle plugin
115
115
  gradle_licenses_dir = File.join(config.root, GRADLE_LICENSES_PATH)
116
- gradle_runner.run("generateLicenseReport", config.source_path)
116
+ gradle_runner.run("generateLicenseReport")
117
117
 
118
118
  # parse the CSV report for dependency license urls
119
119
  CSV.foreach(File.join(gradle_licenses_dir, GRADLE_LICENSES_CSV_NAME), headers: true).each_with_object({}) do |row, hsh|
@@ -134,80 +134,82 @@ module Licensed
134
134
  # The Gradle::Runner class is a wrapper which provides
135
135
  # an interface to run gradle commands with the init script initialized
136
136
  class Runner
137
- def initialize(root_path, configurations, executable)
138
- @root_path = root_path
137
+ def initialize(configurations, executable)
139
138
  @executable = executable
140
- @init_script = create_init_script(root_path, configurations)
139
+ @init_script = create_init_script(configurations)
141
140
  end
142
141
 
143
- def run(command, source_path)
144
- args = [format_command(command, source_path)]
142
+ def run(command)
143
+ args = [command]
145
144
  # The configuration cache is an incubating feature that can be activated manually.
146
145
  # The gradle plugin for licenses does not support it so we prevent it to run for gradle version supporting it.
147
- args << "--no-configuration-cache" if gradle_version >= "6.6"
146
+ args << "--no-configuration-cache" if gradle_version >= Gem::Version.new("6.6")
148
147
  Licensed::Shell.execute(@executable, "-q", "--init-script", @init_script.path, *args)
149
148
  end
150
149
 
151
150
  private
152
151
 
153
- def gradle_version
154
- @gradle_version ||= Licensed::Shell.execute(@executable, "--version").scan(/Gradle [\d+]\.[\d+]/).last.split(" ").last
155
- end
156
-
157
- def create_init_script(path, configurations)
158
- Dir.chdir(path) do
159
- f = Tempfile.new(["init", ".gradle"], @root_path)
160
- f.write(
161
- <<~EOF
162
- import com.github.jk1.license.render.CsvReportRenderer
163
- import com.github.jk1.license.filter.LicenseBundleNormalizer
164
- final configs = #{configurations.inspect}
165
-
166
- initscript {
167
- repositories {
168
- maven {
169
- url "https://plugins.gradle.org/m2/"
170
- }
171
- }
172
- dependencies {
173
- classpath "com.github.jk1:gradle-license-report:#{gradle_version >= "7.0" ? "2.0" : "1.17"}"
152
+ def create_init_script(configurations)
153
+ # we need to create extensions in the event that the user hasn't configured custom configurations
154
+ # to avoid hitting errors where core Gradle configurations are set with canBeResolved=false
155
+ configuration_map = configurations.map { |c| [c, "licensed#{c}"] }.to_h
156
+ configuration_dsl = configuration_map.map { |orig, custom| "#{custom}.extendsFrom(#{orig})" }
157
+
158
+ f = Tempfile.new(["init", ".gradle"])
159
+ f.write(
160
+ <<~EOF
161
+ import com.github.jk1.license.render.CsvReportRenderer
162
+ import com.github.jk1.license.filter.LicenseBundleNormalizer
163
+ final configs = #{configuration_map.values.inspect}
164
+
165
+ initscript {
166
+ repositories {
167
+ maven {
168
+ url "https://plugins.gradle.org/m2/"
174
169
  }
175
170
  }
171
+ dependencies {
172
+ classpath "com.github.jk1:gradle-license-report:#{gradle_version >= Gem::Version.new("7.0") ? "2.0" : "1.17"}"
173
+ }
174
+ }
176
175
 
177
- allprojects {
178
- apply plugin: com.github.jk1.license.LicenseReportPlugin
179
- licenseReport {
180
- outputDir = "$rootDir/.gradle-licenses"
181
- configurations = configs
182
- renderers = [new CsvReportRenderer()]
183
- filters = [new LicenseBundleNormalizer()]
184
- }
176
+ allprojects {
177
+ configurations {
178
+ #{configuration_dsl.join("\n") }
179
+ }
180
+
181
+ apply plugin: com.github.jk1.license.LicenseReportPlugin
182
+ licenseReport {
183
+ outputDir = "$rootDir/#{GRADLE_LICENSES_PATH}"
184
+ configurations = configs
185
+ renderers = [new CsvReportRenderer()]
186
+ filters = [new LicenseBundleNormalizer()]
187
+ }
185
188
 
186
- task printDependencies {
187
- doLast {
188
- def dependencies = []
189
- configs.each {
190
- configurations[it].resolvedConfiguration.resolvedArtifacts.each { artifact ->
191
- def id = artifact.moduleVersion.id
192
- dependencies << "{ \\"group\\": \\"${id.group}\\", \\"name\\": \\"${id.name}\\", \\"version\\": \\"${id.version}\\" }"
193
- }
194
- }
195
- println "[${dependencies.join(", ")}]"
196
- }
189
+ task printDependencies {
190
+ doLast {
191
+ def dependencies = []
192
+ configs.each {
193
+ configurations[it].resolvedConfiguration.resolvedArtifacts.each { artifact ->
194
+ def id = artifact.moduleVersion.id
195
+ dependencies << "{ \\"group\\": \\"${id.group}\\", \\"name\\": \\"${id.name}\\", \\"version\\": \\"${id.version}\\" }"
196
+ }
197
+ }
198
+ println "[${dependencies.join(", ")}]"
197
199
  }
198
200
  }
199
- EOF
200
- )
201
- f.close
202
- f
203
- end
201
+ }
202
+ EOF
203
+ )
204
+ f.close
205
+ f
204
206
  end
205
207
 
206
- # Prefixes the gradle command with the project name for multi-build projects.
207
- def format_command(command, source_path)
208
- Dir.chdir(source_path) do
209
- path = Licensed::Shell.execute(@executable, "properties", "-Dorg.gradle.logging.level=quiet").scan(/path:.*/).last.split(" ").last
210
- path == ":" ? command : "#{path}:#{command}"
208
+ # Returns the version of gradle used during execution
209
+ def gradle_version
210
+ @gradle_version ||= begin
211
+ version = Licensed::Shell.execute(@executable, "--version").scan(/Gradle [\d+]\.[\d+]/).last.split(" ").last
212
+ Gem::Version.new(version)
211
213
  end
212
214
  end
213
215
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+
4
+ module Licensed
5
+ module Sources
6
+ class PNPM < Source
7
+ # The PNPM source requires matching reviewed or ignored dependencies
8
+ # on both name and version
9
+ def self.require_matched_dependency_version
10
+ true
11
+ end
12
+
13
+ # Returns true when pnpm is installed and a pnpm-lock.yaml file is found,
14
+ # otherwise false
15
+ def enabled?
16
+ return false unless Licensed::Shell.tool_available?("pnpm")
17
+ File.exist?(File.join(config.pwd, "pnpm-lock.yaml"))
18
+ end
19
+
20
+ def enumerate_dependencies
21
+ packages.map do |package|
22
+ name_with_version = "#{package["name"]}@#{package["version"]}"
23
+ Dependency.new(
24
+ name: name_with_version,
25
+ version: package["version"],
26
+ path: package["path"],
27
+ metadata: {
28
+ "type" => PNPM.type,
29
+ "name" => package["name"],
30
+ "summary" => package["description"],
31
+ "homepage" => package["homepage"]
32
+ }
33
+ )
34
+ end
35
+ end
36
+
37
+ # Returns package metadata returned from `pnpm licensed list`
38
+ def packages
39
+ JSON.parse(package_metadata_command).values.flatten
40
+ rescue JSON::ParserError => e
41
+ message = "Licensed was unable to parse the output from 'pnpm licenses list'. JSON Error: #{e.message}"
42
+ raise Licensed::Sources::Source::Error, message
43
+ end
44
+
45
+ # Returns the output from running `pnpm licenses list` to get package metadata
46
+ def package_metadata_command
47
+ args = %w(--json --long)
48
+ args << "--prod" unless include_non_production?
49
+ Licensed::Shell.execute("pnpm", "licenses", "list", *args, allow_failure: true)
50
+ end
51
+
52
+ # Returns whether to include non production dependencies based on the licensed configuration settings
53
+ def include_non_production?
54
+ config.dig("pnpm", "production_only") == false
55
+ end
56
+ end
57
+ end
58
+ end
@@ -51,6 +51,12 @@ module Licensed
51
51
  .downcase
52
52
  .split("::")
53
53
  end
54
+
55
+ # Returns true if the source requires matching reviewed and ignored dependencies'
56
+ # versions as well as their name
57
+ def require_matched_dependency_version
58
+ false
59
+ end
54
60
  end
55
61
 
56
62
  # all sources have a configuration
@@ -69,7 +75,9 @@ module Licensed
69
75
  # Returns all dependencies that should be evaluated.
70
76
  # Excludes ignored dependencies.
71
77
  def dependencies
72
- cached_dependencies.reject { |d| ignored?(d) }
78
+ cached_dependencies
79
+ .reject { |d| ignored?(d) }
80
+ .each { |d| add_additional_terms_from_configuration(d) }
73
81
  end
74
82
 
75
83
  # Enumerate all source dependencies. Must be implemented by each source class.
@@ -79,7 +87,7 @@ module Licensed
79
87
 
80
88
  # Returns whether a dependency is ignored in the configuration.
81
89
  def ignored?(dependency)
82
- config.ignored?("type" => self.class.type, "name" => dependency.name)
90
+ config.ignored?(dependency.metadata, require_version: self.class.require_matched_dependency_version)
83
91
  end
84
92
 
85
93
  private
@@ -88,6 +96,11 @@ module Licensed
88
96
  def cached_dependencies
89
97
  @dependencies ||= enumerate_dependencies.compact
90
98
  end
99
+
100
+ # Add any additional_terms for this dependency that have been added to the configuration
101
+ def add_additional_terms_from_configuration(dependency)
102
+ dependency.additional_terms.concat config.additional_terms_for_dependency("type" => self.class.type, "name" => dependency.name)
103
+ end
91
104
  end
92
105
  end
93
106
  end
@@ -6,19 +6,20 @@ module Licensed
6
6
  require "licensed/sources/bundler"
7
7
  require "licensed/sources/cabal"
8
8
  require "licensed/sources/cargo"
9
+ require "licensed/sources/cocoapods"
9
10
  require "licensed/sources/composer"
10
11
  require "licensed/sources/dep"
11
12
  require "licensed/sources/git_submodule"
12
13
  require "licensed/sources/go"
14
+ require "licensed/sources/gradle"
13
15
  require "licensed/sources/manifest"
16
+ require "licensed/sources/mix"
14
17
  require "licensed/sources/npm"
15
18
  require "licensed/sources/nuget"
16
19
  require "licensed/sources/pip"
17
20
  require "licensed/sources/pipenv"
21
+ require "licensed/sources/pnpm"
18
22
  require "licensed/sources/swift"
19
- require "licensed/sources/gradle"
20
- require "licensed/sources/mix"
21
23
  require "licensed/sources/yarn"
22
- require "licensed/sources/cocoapods"
23
24
  end
24
25
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Licensed
3
- VERSION = "4.0.4".freeze
3
+ VERSION = "4.2.0".freeze
4
4
 
5
5
  def self.previous_major_versions
6
6
  major_version = Gem::Version.new(Licensed::VERSION).segments.first
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: licensed
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.4
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-26 00:00:00.000000000 Z
11
+ date: 2023-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: licensee
@@ -219,6 +219,7 @@ files:
219
219
  - docs/commands/version.md
220
220
  - docs/configuration.md
221
221
  - docs/configuration/README.md
222
+ - docs/configuration/additional_terms.md
222
223
  - docs/configuration/allowed_licenses.md
223
224
  - docs/configuration/application_name.md
224
225
  - docs/configuration/application_source.md
@@ -249,6 +250,7 @@ files:
249
250
  - docs/sources/nuget.md
250
251
  - docs/sources/pip.md
251
252
  - docs/sources/pipenv.md
253
+ - docs/sources/pnpm.md
252
254
  - docs/sources/stack.md
253
255
  - docs/sources/swift.md
254
256
  - docs/sources/yarn.md
@@ -298,6 +300,7 @@ files:
298
300
  - lib/licensed/sources/nuget.rb
299
301
  - lib/licensed/sources/pip.rb
300
302
  - lib/licensed/sources/pipenv.rb
303
+ - lib/licensed/sources/pnpm.rb
301
304
  - lib/licensed/sources/source.rb
302
305
  - lib/licensed/sources/swift.rb
303
306
  - lib/licensed/sources/yarn.rb