licensed 4.0.4 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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