licensed 2.9.0 → 2.11.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,17 +6,55 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## 2.11.1
10
+ 2020-06-09
11
+
12
+ ### Fixed
13
+ - `notices` command properly reads cached dependency notices contents (https://github.com/github/licensed/pull/283)
14
+
15
+ ## 2.11.0
16
+ 2020-06-02
17
+
18
+ ### Added
19
+ - `notices` command to create a `NOTICE` file for each configured app (https://github.com/github/licensed/pull/277)
20
+
21
+ ### Fixed
22
+ - NuGet source no longer crashes on a non-existent dependency path (https://github.com/github/licensed/pull/280)
23
+ - Go source no longer crashes on a non-existent dependency package path (https://github.com/github/licensed/pull/274)
24
+
25
+ ## 2.10.0
26
+ 2020-05-15
27
+
28
+ ### Changed
29
+ - NPM source ignores missing peer dependencies (https://github.com/github/licensed/pull/267)
30
+
31
+ ### Added
32
+ - NuGet source (:tada: @zarenner https://github.com/github/licensed/pull/261)
33
+ - Multiple apps can share a single cache location (https://github.com/github/licensed/pull/263)
34
+
35
+ ## 2.9.2
36
+ 2020-04-28
37
+
38
+ ### Changed
39
+ - `licensee` minimum version bumped to 9.13.2 (https://github.com/github/licensed/pull/256)
40
+
41
+ ## 2.9.1
42
+ 2020-03-24
43
+
44
+ ### Changed
45
+ - relaxed gem version restrictions on Thor (:tada: @eileencodes https://github.com/github/licensed/pull/254)
46
+
9
47
  ## 2.9.0
10
48
  2020-03-19
11
49
 
12
- ## Added
50
+ ### Added
13
51
  - Source paths use glob pattern matching (https://github.com/github/licensed/pull/245)
14
52
 
15
- ## Fixed
53
+ ### Fixed
16
54
  - Mix source supports updates to mix.lock format (:tada: @bruce https://github.com/github/licensed/pull/242)
17
55
  - Go source supports `go list` format changes in go 1.14 (https://github.com/github/licensed/pull/247)
18
56
 
19
- ## Changed
57
+ ### Changed
20
58
  - `licensed cache` will flag dependencies for re-review when license text changes (https://github.com/github/licensed/pull/248)
21
59
  - `licensed status` will raise errors on dependencies that need re-review (https://github.com/github/licensed/pull/248)
22
60
  - `licensee` minimum version bumped to 9.13.1 (https://github.com/github/licensed/pull/251)
@@ -280,4 +318,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
280
318
 
281
319
  Initial release :tada:
282
320
 
283
- [Unreleased]: https://github.com/github/licensed/compare/2.9.0...HEAD
321
+ [Unreleased]: https://github.com/github/licensed/compare/2.11.1...HEAD
data/README.md CHANGED
@@ -24,13 +24,13 @@ See the [migration documentation](./docs/migrating_to_newer_versions.md) for mor
24
24
  ### Dependencies
25
25
 
26
26
  Licensed uses the `libgit2` bindings for Ruby provided by `rugged`. `rugged` requires `cmake` and `pkg-config` which you may need to install before you can install Licensed.
27
-
27
+
28
28
  > Ubuntu
29
-
29
+
30
30
  sudo apt-get install cmake pkg-config
31
-
31
+
32
32
  > OS X
33
-
33
+
34
34
  brew install cmake pkg-config
35
35
 
36
36
  ### With a Gemfile
@@ -64,8 +64,10 @@ For system wide usage, install licensed to a location on `$PATH`, e.g. `/usr/loc
64
64
 
65
65
  - `licensed list`: Output enumerated dependencies only.
66
66
  - `licensed cache`: Cache licenses and metadata.
67
- - `licensed status`: Check status of dependencies' cached licenses. For example:
67
+ - `licensed status`: Check status of dependencies' cached licenses.
68
+ - `licensed notices`: Write a `NOTICE` file for each application configuration.
68
69
  - `licensed version`: Show current installed version of Licensed. Aliases: `-v|--version`
70
+ - `licensed env`: Output environment information from the licensed configuration.
69
71
 
70
72
  See the [commands documentation](./docs/commands.md) for additional documentation, or run `licensed -h` to see all of the current available commands.
71
73
 
@@ -102,14 +104,16 @@ Dependencies will be automatically detected for all of the following sources by
102
104
  1. [Bundler](./docs/sources/bundler.md)
103
105
  1. [Cabal](./docs/sources/cabal.md)
104
106
  1. [Composer](./docs/sources/composer.md)
107
+ 1. [Git Submodules (git_submodule)](./docs/sources/git_submodule.md)
105
108
  1. [Go](./docs/sources/go.md)
106
109
  1. [Go Dep (dep)](./docs/sources/dep.md)
110
+ 1. [Gradle](./docs/sources/gradle.md)
107
111
  1. [Manifest lists (manifests)](./docs/sources/manifests.md)
112
+ 1. [Mix](./docs/sources/mix.md)
108
113
  1. [NPM](./docs/sources/npm.md)
114
+ 1. [NuGet](./docs/sources/nuget.md)
109
115
  1. [Pip](./docs/sources/pip.md)
110
116
  1. [Pipenv](./docs/sources/pipenv.md)
111
- 1. [Git Submodules (git_submodule)](./docs/sources/git_submodule.md)
112
- 1. [Mix](./docs/sources/mix.md)
113
117
  1. [Yarn](./docs/sources/yarn.md)
114
118
 
115
119
  You can disable any of them in the configuration file:
@@ -31,6 +31,12 @@ A dependency will fail the status checks if:
31
31
  5. The cached record is flagged for re-review.
32
32
  - This occurs when the record's license text has changed since the record was reviewed.
33
33
 
34
+ ## `notices`
35
+
36
+ Outputs license and notice text for all dependencies in each app into a `NOTICE` file in the app's `cache_path`. If an app uses a shared cache path, the file name will contain the app name as well, e.g. `NOTICE.my_app`.
37
+
38
+ The `NOTICE` file contents are retrieved from cached records, with the assumption that cached records have already been reviewed in a compliance workflow.
39
+
34
40
  ## `env`
35
41
 
36
42
  Prints the runtime environment used by licensed after loading a configuration file. By default the output is in YAML format, but can be output in JSON using the `--json` flag.
@@ -209,6 +209,26 @@ apps:
209
209
 
210
210
  In this example, the root configuration will contain a default cache path of `.licenses`. `app1` will inherit this value and append it's name, resulting in a cache path of `.licenses/app1`.
211
211
 
212
+ ### Sharing caches between apps
213
+
214
+ Dependency caches can be shared between apps by setting the same cache path on each app.
215
+
216
+ ```yaml
217
+ apps:
218
+ - source_path: "path/to/app1"
219
+ cache_path: ".licenses/apps"
220
+ - source_path: "path/to/app2"
221
+ cache_path: ".licenses/apps"
222
+ ```
223
+
224
+ When using a source path with a glob pattern, the apps created from the glob pattern can share a dependency by setting an explicit cache path and setting `shared_cache` to true.
225
+
226
+ ```yaml
227
+ source_path: "path/to/apps/*"
228
+ cache_path: ".licenses/apps"
229
+ shared_cache: true
230
+ ```
231
+
212
232
  ## Source specific configuration
213
233
 
214
234
  See the [source documentation](./sources) for details on any source specific configuration.
@@ -0,0 +1,14 @@
1
+ # NuGet
2
+
3
+ The NuGet source will detect ProjectReference-style restored packages by inspecting `project.assets.json` files for dependencies. It requires that `dotnet restore` has already ran on the project.
4
+
5
+ The source currently expects that `source_path` is set to the `obj` directory containing the `project.assets.json`.
6
+ For example, if your project lives at `foo/foo.proj`, you likely want to set `source_path` to `foo/obj`.
7
+ If in MSBuild you have customized your `obj` paths (e.g. to live outside your source tree), you may need to set `source_path` to something different such as `../obj/foo`.
8
+
9
+ ### Search strategy
10
+ This source looks for licenses:
11
+ 1. Specified by SPDX expression via `<license type="expression">` in a package's `.nuspec` (via licensee)
12
+ 2. In license files such as `LICENSE.txt`, even if not specified in the `.nuspec` (via licensee)
13
+ 3. Specified by filepath via `<license type="file">` in a package's `.nuspec`, even if not a standard license filename.
14
+ 4. By downloading and inspecting the contents of `<licenseUrl>` in a package's `.nuspec`, if not found otherwise.
@@ -28,6 +28,13 @@ module Licensed
28
28
  run Licensed::Commands::List.new(config: config)
29
29
  end
30
30
 
31
+ desc "notices", "Generate a NOTICE file from cached records"
32
+ method_option :config, aliases: "-c", type: :string,
33
+ desc: "Path to licensed configuration file"
34
+ def notices
35
+ run Licensed::Commands::Notices.new(config: config)
36
+ end
37
+
31
38
  map "-v" => :version
32
39
  map "--version" => :version
33
40
  desc "version", "Show Installed Version of Licensed, [-v, --version]"
@@ -6,5 +6,6 @@ module Licensed
6
6
  require "licensed/commands/status"
7
7
  require "licensed/commands/list"
8
8
  require "licensed/commands/environment"
9
+ require "licensed/commands/notices"
9
10
  end
10
11
  end
@@ -11,20 +11,39 @@ module Licensed
11
11
  Licensed::Reporters::CacheReporter.new
12
12
  end
13
13
 
14
+ # Run the command.
15
+ # Removes any cached records that don't match a current application
16
+ # dependency.
17
+ #
18
+ # options - Options to run the command with
19
+ #
20
+ # Returns whether the command was a success
21
+ def run(**options)
22
+ begin
23
+ result = super
24
+ clear_stale_cached_records if result
25
+
26
+ result
27
+ ensure
28
+ cache_paths.clear
29
+ files.clear
30
+ end
31
+ end
32
+
14
33
  protected
15
34
 
16
- # Run the command for all enumerated dependencies found in a dependency source,
35
+ # Run the command for all enabled sources for an application configuration,
17
36
  # recording results in a report.
18
- # Removes any cached records that don't match a current application
19
- # dependency.
20
37
  #
21
- # app - The application configuration for the source
22
- # source - A dependency source enumerator
38
+ # app - An application configuration
23
39
  #
24
- # Returns whether the command succeeded for the dependency source enumerator
25
- def run_source(app, source)
40
+ # Returns whether the command succeeded for the application.
41
+ def run_app(app)
26
42
  result = super
27
- clear_stale_cached_records(app, source) if result
43
+
44
+ # add the full cache path to the list of cache paths evaluted during this run
45
+ cache_paths << app.cache_path
46
+
28
47
  result
29
48
  end
30
49
 
@@ -62,6 +81,9 @@ module Licensed
62
81
  report.warnings << "expected dependency path #{dependency.path} does not exist"
63
82
  end
64
83
 
84
+ # add the absolute dependency file path to the list of files seen during this licensed run
85
+ files << filename.to_s
86
+
65
87
  true
66
88
  end
67
89
 
@@ -86,18 +108,26 @@ module Licensed
86
108
 
87
109
  # Clean up cached files that dont match current dependencies
88
110
  #
89
- # app - An application configuration
90
- # source - A dependency source enumerator
91
- #
92
111
  # Returns nothing
93
- def clear_stale_cached_records(app, source)
94
- names = source.dependencies.map { |dependency| File.join(source.class.type, dependency.name) }
95
- Dir.glob(app.cache_path.join(source.class.type, "**/*.#{DependencyRecord::EXTENSION}")).each do |file|
96
- file_path = Pathname.new(file)
97
- relative_path = file_path.relative_path_from(app.cache_path).to_s
98
- FileUtils.rm(file) unless names.include?(relative_path.chomp(".#{DependencyRecord::EXTENSION}"))
112
+ def clear_stale_cached_records
113
+ cache_paths.each do |cache_path|
114
+ Dir.glob(cache_path.join("**/*.#{DependencyRecord::EXTENSION}")).each do |file|
115
+ next if files.include?(file)
116
+
117
+ FileUtils.rm(file)
118
+ end
99
119
  end
100
120
  end
121
+
122
+ # Set of unique cache paths that are evaluted during the run
123
+ def cache_paths
124
+ @cache_paths ||= Set.new
125
+ end
126
+
127
+ # Set of unique absolute file paths of cached records evaluted during the run
128
+ def files
129
+ @files ||= Set.new
130
+ end
101
131
  end
102
132
  end
103
133
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module Licensed
3
+ module Commands
4
+ class Notices < Command
5
+ # Create a reporter to use during a command run
6
+ #
7
+ # options - The options the command was run with
8
+ #
9
+ # Raises a Licensed::Reporters::CacheReporter
10
+ def create_reporter(options)
11
+ Licensed::Reporters::NoticesReporter.new
12
+ end
13
+
14
+ protected
15
+
16
+ # Load stored dependency record data to add to the notices report.
17
+ #
18
+ # app - The application configuration for the dependency
19
+ # source - The dependency source enumerator for the dependency
20
+ # dependency - An application dependency
21
+ # report - A report hash for the command to provide extra data for the report output.
22
+ #
23
+ # Returns true.
24
+ def evaluate_dependency(app, source, dependency, report)
25
+ filename = app.cache_path.join(source.class.type, "#{dependency.name}.#{DependencyRecord::EXTENSION}")
26
+ report["cached_record"] = Licensed::DependencyRecord.read(filename)
27
+ if !report["cached_record"]
28
+ report["warning"] = "expected cached record not found at #{filename}"
29
+ end
30
+
31
+ true
32
+ end
33
+ end
34
+ end
35
+ end
@@ -108,7 +108,12 @@ module Licensed
108
108
  def detect_cache_path(options, inherited_options)
109
109
  return options["cache_path"] unless options["cache_path"].to_s.empty?
110
110
 
111
- cache_path = inherited_options["cache_path"] || DEFAULT_CACHE_PATH
111
+ # if cache_path and shared_cache are both set in inherited_options,
112
+ # don't append the app name to the cache path
113
+ cache_path = inherited_options["cache_path"]
114
+ return cache_path if cache_path && inherited_options["shared_cache"] == true
115
+
116
+ cache_path ||= DEFAULT_CACHE_PATH
112
117
  File.join(cache_path, self["name"])
113
118
  end
114
119
 
@@ -167,7 +172,12 @@ module Licensed
167
172
  # will handle configurations that don't have these explicitly set
168
173
  dir_name = File.basename(path)
169
174
  config["name"] = "#{config["name"]}-#{dir_name}" if config["name"]
170
- config["cache_path"] = File.join(config["cache_path"], dir_name) if config["cache_path"]
175
+
176
+ # if a cache_path is set and is not marked as shared, append the app name
177
+ # to the end of the cache path to make a unique cache path for the app
178
+ if config["cache_path"] && config["shared_cache"] != true
179
+ config["cache_path"] = File.join(config["cache_path"], dir_name)
180
+ end
171
181
 
172
182
  config
173
183
  end
@@ -74,7 +74,7 @@ module Licensed
74
74
  def license_contents
75
75
  files = matched_files.reject { |f| f == package_file }
76
76
  .group_by(&:content)
77
- .map { |content, files| { "sources" => license_content_sources(files), "text" => content } }
77
+ .map { |content, sources| { "sources" => license_content_sources(sources), "text" => content } }
78
78
 
79
79
  files << generated_license_contents if files.empty?
80
80
  files.compact
@@ -7,5 +7,6 @@ module Licensed
7
7
  require "licensed/reporters/list_reporter"
8
8
  require "licensed/reporters/json_reporter"
9
9
  require "licensed/reporters/yaml_reporter"
10
+ require "licensed/reporters/notices_reporter"
10
11
  end
11
12
  end
@@ -27,32 +27,32 @@ module Licensed
27
27
  shell.info " #{source.class.type}"
28
28
  result = yield report
29
29
 
30
- warning_reports = report.all_reports.select { |report| report.warnings.any? }.to_a
30
+ warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
31
31
  if warning_reports.any?
32
32
  shell.newline
33
33
  shell.warn " * Warnings:"
34
- warning_reports.each do |report|
35
- display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
34
+ warning_reports.each do |r|
35
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
36
36
 
37
- shell.warn " * #{report.name}"
37
+ shell.warn " * #{r.name}"
38
38
  shell.warn " #{display_metadata}" unless display_metadata.empty?
39
- report.warnings.each do |warning|
39
+ r.warnings.each do |warning|
40
40
  shell.warn " - #{warning}"
41
41
  end
42
42
  shell.newline
43
43
  end
44
44
  end
45
45
 
46
- errored_reports = report.all_reports.select { |report| report.errors.any? }.to_a
46
+ errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
47
47
  if errored_reports.any?
48
48
  shell.newline
49
49
  shell.error " * Errors:"
50
- errored_reports.each do |report|
51
- display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
50
+ errored_reports.each do |r|
51
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
52
52
 
53
- shell.error " * #{report.name}"
53
+ shell.error " * #{r.name}"
54
54
  shell.error " #{display_metadata}" unless display_metadata.empty?
55
- report.errors.each do |error|
55
+ r.errors.each do |error|
56
56
  shell.error " - #{error}"
57
57
  end
58
58
  shell.newline
@@ -28,16 +28,16 @@ module Licensed
28
28
  shell.info " #{source.class.type}"
29
29
  result = yield report
30
30
 
31
- errored_reports = report.all_reports.select { |report| report.errors.any? }.to_a
31
+ errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
32
32
  if errored_reports.any?
33
33
  shell.newline
34
34
  shell.error " * Errors:"
35
- errored_reports.each do |report|
36
- display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
35
+ errored_reports.each do |r|
36
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
37
37
 
38
- shell.error " * #{report.name}"
38
+ shell.error " * #{r.name}"
39
39
  shell.error " #{display_metadata}" unless display_metadata.empty?
40
- report.errors.each do |error|
40
+ r.errors.each do |error|
41
41
  shell.error " - #{error}"
42
42
  end
43
43
  shell.newline
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ module Reporters
5
+ class NoticesReporter < Reporter
6
+ TEXT_SEPARATOR = "\n\n#{("-" * 5)}\n\n".freeze
7
+ LICENSE_SEPARATOR = "\n#{("*" * 5)}\n".freeze
8
+
9
+ # Reports on an application configuration in a notices command run
10
+ #
11
+ # app - An application configuration
12
+ #
13
+ # Returns the result of the yielded method
14
+ # Note - must be called from inside the `report_run` scope
15
+ def report_app(app)
16
+ super do |report|
17
+ filename = app["shared_cache"] ? "NOTICE.#{app["name"]}" : "NOTICE"
18
+ path = app.cache_path.join(filename)
19
+ shell.info "Writing notices for #{app["name"]} to #{path}"
20
+
21
+ result = yield report
22
+
23
+ File.open(path, "w") do |file|
24
+ file << "THIRD PARTY NOTICES\n"
25
+ file << LICENSE_SEPARATOR
26
+ file << report.all_reports
27
+ .map { |r| notices(r) }
28
+ .compact
29
+ .join(LICENSE_SEPARATOR)
30
+ end
31
+
32
+ result
33
+ end
34
+ end
35
+
36
+ # Reports on a dependency in a notices command run.
37
+ #
38
+ # dependency - An application dependency
39
+ #
40
+ # Returns the result of the yielded method
41
+ # Note - must be called from inside the `report_run` scope
42
+ def report_dependency(dependency)
43
+ super do |report|
44
+ result = yield report
45
+ shell.warn "* #{report["warning"]}" if report["warning"]
46
+ result
47
+ end
48
+ end
49
+
50
+ # Returns notices information for a dependency report
51
+ def notices(report)
52
+ return unless report.target.is_a?(Licensed::Dependency)
53
+
54
+ cached_record = report["cached_record"]
55
+ return unless cached_record
56
+
57
+ texts = cached_record.licenses.map(&:text)
58
+ cached_record.notices.each do |notice|
59
+ case notice
60
+ when Hash
61
+ texts << notice["text"]
62
+ when String
63
+ texts << notice
64
+ else
65
+ shell.warn "* unable to parse notices for #{report.target.name}"
66
+ end
67
+ end
68
+
69
+ <<~NOTICE
70
+ #{cached_record["name"]}@#{cached_record["version"]}
71
+
72
+ #{texts.map(&:strip).reject(&:empty?).compact.join(TEXT_SEPARATOR)}
73
+ NOTICE
74
+ end
75
+ end
76
+ end
77
+ end