licensed 4.1.0 → 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: 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