licensed 4.1.0 → 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: 7606d0b5e5f3755ee329a1963cda970c021deab08680c619731ed6fb3ba547da
4
- data.tar.gz: 668a2d87d8019284b6ce02bccdda851ad186f03cc7d389fbdd659473affc08cf
3
+ metadata.gz: 2b65e198a420b03486b2680a6a83fb04b4e67684d28bc4ba9ef00c466ffd7489
4
+ data.tar.gz: ab2b10c6e854d3f1d7faa918e48addb497032838fd1cea942ff823053b891150
5
5
  SHA512:
6
- metadata.gz: '064129baadae7345b5c05e2635cc1850b8ce8321f1e2df803b5fc6d6704556a1337c7d1561024775801cf5cb4158b6c25657b06a0a9baf5ccac7a7453f35fa53'
7
- data.tar.gz: b3c6ba7179d7b777665f29b5cace4536181a428a5995c5e7d1d168b4ba6012fd333d191eb6ce23ab7b971a9cb8231dbfb999743c72bf91a1efe78ddec78223b7
6
+ metadata.gz: 8dee38c45e73cb03b7c94a9260bb9bc6f5919f53156b69a751238693920e2f120a3dcb0d43f66270fa9abc8305705fdced290978e51bfe762be5f0e5ba00230d
7
+ data.tar.gz: d352b46e40f545f0bd3e1aa29e3bb62454e2e329c64ce9197ba1f9c38b4f11bcbbbecc789658dcff0e6b79e43b4ee9cc839b5476e23cab44a559a605ddbd77b6
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ 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
+
9
19
  ## 4.1.0
10
20
 
11
21
  ### Added
@@ -713,4 +723,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
713
723
 
714
724
  Initial release :tada:
715
725
 
716
- [Unreleased]: https://github.com/github/licensed/compare/4.1.0...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.1.0)
4
+ licensed (4.2.0)
5
5
  json (~> 2.6)
6
6
  licensee (~> 9.16)
7
7
  parallel (~> 1.22)
@@ -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
data/docs/sources/pnpm.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
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
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
+
5
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).
6
8
 
7
9
  ## Including development dependencies
@@ -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
@@ -117,15 +121,41 @@ module Licensed
117
121
 
118
122
  private
119
123
 
120
- def any_list_pattern_matched?(list, dependency, match_version: false)
124
+ def any_list_pattern_matched?(list, dependency, require_version: false)
121
125
  Array(list).any? do |pattern|
122
- if match_version
123
- at_version = "@#{dependency["version"]}"
124
- pattern, pattern_version = pattern.rpartition(at_version).values_at(0, 1)
125
- 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
126
141
  end
127
142
 
128
- 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
129
159
  end
130
160
  end
131
161
 
@@ -99,6 +99,17 @@ module Licensed
99
99
  .select { |notice| notice["text"].length > 0 } # files with content only
100
100
  end
101
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
+
102
113
  private
103
114
 
104
115
  def read_file_with_encoding_check(file_path)
@@ -135,14 +146,8 @@ module Licensed
135
146
  # Returns the metadata that represents this dependency. This metadata
136
147
  # is written to YAML in the dependencys cached text file
137
148
  def license_metadata
138
- {
139
- # can be overriden by values in @metadata
140
- "name" => name,
141
- "version" => version
142
- }.merge(
143
- @metadata
144
- ).merge({
145
- # overrides all other values
149
+ metadata.merge({
150
+ # overrides all metadata values
146
151
  "license" => license_key
147
152
  })
148
153
  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
@@ -4,6 +4,12 @@ require "json"
4
4
  module Licensed
5
5
  module Sources
6
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
+
7
13
  # Returns true when pnpm is installed and a pnpm-lock.yaml file is found,
8
14
  # otherwise false
9
15
  def enabled?
@@ -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
@@ -81,7 +87,7 @@ module Licensed
81
87
 
82
88
  # Returns whether a dependency is ignored in the configuration.
83
89
  def ignored?(dependency)
84
- config.ignored?("type" => self.class.type, "name" => dependency.name)
90
+ config.ignored?(dependency.metadata, require_version: self.class.require_matched_dependency_version)
85
91
  end
86
92
 
87
93
  private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Licensed
3
- VERSION = "4.1.0".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.1.0
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-02-08 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