licensed 3.1.0 → 3.2.3

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +19 -0
  3. data/.github/workflows/release.yml +4 -4
  4. data/.github/workflows/test.yml +169 -48
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +51 -1
  7. data/README.md +25 -80
  8. data/docker/Dockerfile.build-linux +1 -1
  9. data/docs/adding_a_new_source.md +11 -8
  10. data/docs/commands/README.md +59 -0
  11. data/docs/commands/cache.md +35 -0
  12. data/docs/commands/env.md +10 -0
  13. data/docs/commands/list.md +23 -0
  14. data/docs/commands/migrate.md +10 -0
  15. data/docs/commands/notices.md +12 -0
  16. data/docs/commands/status.md +74 -0
  17. data/docs/commands/version.md +3 -0
  18. data/docs/configuration/README.md +11 -0
  19. data/docs/configuration/allowed_licenses.md +17 -0
  20. data/docs/configuration/application_name.md +63 -0
  21. data/docs/configuration/application_source.md +64 -0
  22. data/docs/configuration/configuration_root.md +27 -0
  23. data/docs/configuration/configuring_multiple_apps.md +58 -0
  24. data/docs/configuration/dependency_source_enumerators.md +28 -0
  25. data/docs/configuration/ignoring_dependencies.md +19 -0
  26. data/docs/configuration/metadata_cache.md +106 -0
  27. data/docs/configuration/reviewing_dependencies.md +18 -0
  28. data/docs/configuration.md +9 -173
  29. data/lib/licensed/cli.rb +2 -2
  30. data/lib/licensed/commands/cache.rb +21 -20
  31. data/lib/licensed/commands/command.rb +108 -73
  32. data/lib/licensed/commands/environment.rb +12 -11
  33. data/lib/licensed/commands/list.rb +0 -19
  34. data/lib/licensed/commands/notices.rb +0 -19
  35. data/lib/licensed/commands/status.rb +13 -15
  36. data/lib/licensed/configuration.rb +77 -7
  37. data/lib/licensed/report.rb +44 -0
  38. data/lib/licensed/reporters/cache_reporter.rb +48 -64
  39. data/lib/licensed/reporters/json_reporter.rb +19 -21
  40. data/lib/licensed/reporters/list_reporter.rb +45 -58
  41. data/lib/licensed/reporters/notices_reporter.rb +33 -46
  42. data/lib/licensed/reporters/reporter.rb +37 -104
  43. data/lib/licensed/reporters/status_reporter.rb +58 -56
  44. data/lib/licensed/reporters/yaml_reporter.rb +19 -21
  45. data/lib/licensed/sources/bundler/definition.rb +36 -0
  46. data/lib/licensed/sources/bundler/missing_specification.rb +10 -7
  47. data/lib/licensed/sources/bundler.rb +34 -70
  48. data/lib/licensed/sources/dep.rb +2 -2
  49. data/lib/licensed/sources/go.rb +3 -3
  50. data/lib/licensed/sources/gradle.rb +2 -2
  51. data/lib/licensed/sources/helpers/content_versioning.rb +2 -1
  52. data/lib/licensed/sources/npm.rb +4 -3
  53. data/lib/licensed/sources/nuget.rb +1 -2
  54. data/lib/licensed/version.rb +1 -1
  55. data/lib/licensed.rb +1 -0
  56. data/licensed.gemspec +4 -4
  57. data/script/source-setup/go +1 -1
  58. metadata +45 -13
  59. data/docs/commands.md +0 -95
@@ -3,9 +3,14 @@ require "pathname"
3
3
 
4
4
  module Licensed
5
5
  class AppConfiguration < Hash
6
+ DIRECTORY_NAME_GENERATOR_KEY = "directory_name".freeze
7
+ RELATIVE_PATH_GENERATOR_KEY = "relative_path".freeze
8
+ DEFAULT_RELATIVE_PATH_NAME_SEPARATOR = "-".freeze
9
+ ALL_NAME_GENERATOR_KEYS = [DIRECTORY_NAME_GENERATOR_KEY, RELATIVE_PATH_GENERATOR_KEY].freeze
10
+
6
11
  DEFAULT_CACHE_PATH = ".licenses".freeze
7
12
 
8
- # Returns the root for a configuration in following order of precendence:
13
+ # Returns the root for a configuration in following order of precedence:
9
14
  # 1. explicitly configured "root" property
10
15
  # 2. a found git repository root
11
16
  # 3. the current directory
@@ -28,9 +33,9 @@ module Licensed
28
33
  self["ignored"] ||= {}
29
34
  self["allowed"] ||= []
30
35
  self["root"] = AppConfiguration.root_for(self)
31
- # defaults to the directory name of the source path if not set
32
- self["name"] ||= File.basename(self["source_path"])
33
- # setting the cache path might need a valid app name
36
+ self["name"] = generate_app_name
37
+ # setting the cache path might need a valid app name.
38
+ # this must come after setting self["name"]
34
39
  self["cache_path"] = detect_cache_path(options, inherited_options)
35
40
  end
36
41
 
@@ -105,8 +110,9 @@ module Licensed
105
110
 
106
111
  # Returns the cache path for the application based on:
107
112
  # 1. An explicitly set cache path for the application, if set
108
- # 2. An inherited root cache path joined with the app name
109
- # 3. The default cache path joined with the app name
113
+ # 2. An inherited shared cache path
114
+ # 3. An inherited cache path joined with the app name if not shared
115
+ # 4. The default cache path joined with the app name
110
116
  def detect_cache_path(options, inherited_options)
111
117
  return options["cache_path"] unless options["cache_path"].to_s.empty?
112
118
 
@@ -124,6 +130,65 @@ module Licensed
124
130
  raise Licensed::Configuration::LoadError,
125
131
  "App #{self["name"]} is missing required property #{property}"
126
132
  end
133
+
134
+ # Returns a name for the application as one of:
135
+ # 1. An explicitly configured app name, if set
136
+ # 2. A generated app name based on an configured "name" options hash
137
+ # 3. A default value - the source_path directory name
138
+ def generate_app_name
139
+ # use default_app_name if a name value is not set
140
+ return source_path_directory_app_name if self["name"].to_s.empty?
141
+ # keep the same name value unless a hash is given with naming options
142
+ return self["name"] unless self["name"].is_a?(Hash)
143
+
144
+ generator = self.dig("name", "generator")
145
+ case generator
146
+ when nil, DIRECTORY_NAME_GENERATOR_KEY
147
+ source_path_directory_app_name
148
+ when RELATIVE_PATH_GENERATOR_KEY
149
+ relative_path_app_name
150
+ else
151
+ raise Licensed::Configuration::LoadError,
152
+ "Invalid value configured for name.generator: #{generator}. Value must be one of #{ALL_NAME_GENERATOR_KEYS.join(",")}"
153
+ end
154
+ end
155
+
156
+ # Returns an app name from the directory name of the configured source path
157
+ def source_path_directory_app_name
158
+ File.basename(self["source_path"])
159
+ end
160
+
161
+ # Returns an app name from the relative path from the configured app root
162
+ # to the configured app source path.
163
+ def relative_path_app_name
164
+ source_path_parts = File.expand_path(self["source_path"]).split("/")
165
+ root_path_parts = File.expand_path(self["root"]).split("/")
166
+
167
+ # if the source path is equivalent to the root path,
168
+ # return the root directory name
169
+ return root_path_parts[-1] if source_path_parts == root_path_parts
170
+
171
+ if source_path_parts[0..root_path_parts.size-1] != root_path_parts
172
+ raise Licensed::Configuration::LoadError,
173
+ "source_path must be a descendent of the app root to generate an app name from the relative source_path"
174
+ end
175
+
176
+ name_parts = source_path_parts[root_path_parts.size..-1]
177
+
178
+ separator = self.dig("name", "separator") || DEFAULT_RELATIVE_PATH_NAME_SEPARATOR
179
+ depth = self.dig("name", "depth") || 0
180
+ if depth < 0
181
+ raise Licensed::Configuration::LoadError, "name.depth configuration value cannot be less than -1"
182
+ end
183
+
184
+ # offset the depth value by -1 to work as an offset from the end of the array
185
+ # 0 becomes -1, with a start index of (-1 - -1) = 0, or the full array
186
+ # 1 becomes 0, with a start index of (-1 - 0) = -1, or only the last element
187
+ # and so on...
188
+ depth = depth - 1
189
+ start_index = depth >= name_parts.length ? 0 : -1 - depth
190
+ name_parts[start_index..-1].join(separator)
191
+ end
127
192
  end
128
193
 
129
194
  class Configuration
@@ -151,6 +216,11 @@ module Licensed
151
216
  def initialize(options = {})
152
217
  apps = options.delete("apps") || []
153
218
  apps << default_options.merge(options) if apps.empty?
219
+
220
+ # apply a root setting to all app configurations so that it's available
221
+ # when expanding app source paths
222
+ apps.each { |app| app["root"] ||= options["root"] if options["root"] }
223
+
154
224
  apps = apps.flat_map { |app| self.class.expand_app_source_path(app) }
155
225
  @apps = apps.map { |app| AppConfiguration.new(app, options) }
156
226
  end
@@ -199,7 +269,7 @@ module Licensed
199
269
  # will handle configurations that don't have these explicitly set
200
270
  configs.each do |config|
201
271
  dir_name = File.basename(config["source_path"])
202
- config["name"] = "#{config["name"]}-#{dir_name}" if config["name"]
272
+ config["name"] = "#{config["name"]}-#{dir_name}" if config["name"].is_a?(String)
203
273
 
204
274
  # if a cache_path is set and is not marked as shared, append the app name
205
275
  # to the end of the cache path to make a unique cache path for the app
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ class Report < Hash
5
+ attr_reader :name
6
+ attr_reader :target
7
+ def initialize(name:, target:)
8
+ super()
9
+ @name = name
10
+ @target = target
11
+ end
12
+
13
+ def reports
14
+ @reports ||= []
15
+ end
16
+
17
+ def errors
18
+ @errors ||= []
19
+ end
20
+
21
+ def warnings
22
+ @warnings ||= []
23
+ end
24
+
25
+ def all_reports
26
+ result = []
27
+ result << self
28
+ result.push(*reports.flat_map(&:all_reports))
29
+ end
30
+
31
+ # Returns the data from the report as a hash
32
+ def to_h
33
+ # add name, errors and warnings if they have real data
34
+ output = {}
35
+ output["name"] = name unless name.to_s.empty?
36
+ output["errors"] = errors.dup if errors.any?
37
+ output["warnings"] = warnings.dup if warnings.any?
38
+
39
+ # merge the hash data from the report. command-specified data always
40
+ # overwrites local data
41
+ output.merge(super)
42
+ end
43
+ end
44
+ end
@@ -2,89 +2,73 @@
2
2
  module Licensed
3
3
  module Reporters
4
4
  class CacheReporter < Reporter
5
- # Reports on an application configuration in a cache command run
5
+ # Reports the start of caching records for an app
6
6
  #
7
7
  # app - An application configuration
8
- #
9
- # Returns the result of the yielded method
10
- # Note - must be called from inside the `report_run` scope
11
- def report_app(app)
12
- super do |report|
13
- shell.info "Caching dependency records for #{app["name"]}"
14
- yield report
15
- end
8
+ # report - A report containing information about the app evaluation
9
+ def begin_report_app(app, report)
10
+ shell.info "Caching dependency records for #{app["name"]}"
16
11
  end
17
12
 
18
- # Reports on a dependency source enumerator in a cache command run.
19
- # Shows the type and count of dependencies found by the source.
13
+ # Reports the start of caching records for a dependency source
20
14
  #
21
15
  # source - A dependency source enumerator
22
- #
23
- # Returns the result of the yielded method
24
- # Note - must be called from inside the `report_run` scope
25
- def report_source(source)
26
- super do |report|
27
- shell.info " #{source.class.type}"
28
- result = yield report
16
+ # report - A report containing information about the source evaluation
17
+ def begin_report_source(source, report)
18
+ shell.info " #{source.class.type}"
19
+ end
29
20
 
30
- warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
31
- if warning_reports.any?
32
- shell.newline
33
- shell.warn " * Warnings:"
34
- warning_reports.each do |r|
35
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
21
+ # Reports warnings and errors found while evaluating the dependency source
22
+ #
23
+ # source - A dependency source enumerator
24
+ # report - A report containing information about the source evaluation
25
+ def end_report_source(source, report)
26
+ warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
27
+ if warning_reports.any?
28
+ shell.newline
29
+ shell.warn " * Warnings:"
30
+ warning_reports.each do |r|
31
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
36
32
 
37
- shell.warn " * #{r.name}"
38
- shell.warn " #{display_metadata}" unless display_metadata.empty?
39
- r.warnings.each do |warning|
40
- shell.warn " - #{warning}"
41
- end
42
- shell.newline
33
+ shell.warn " * #{r.name}"
34
+ shell.warn " #{display_metadata}" unless display_metadata.empty?
35
+ r.warnings.each do |warning|
36
+ shell.warn " - #{warning}"
43
37
  end
38
+ shell.newline
44
39
  end
40
+ end
45
41
 
46
- errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
47
- if errored_reports.any?
48
- shell.newline
49
- shell.error " * Errors:"
50
- errored_reports.each do |r|
51
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
42
+ errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
43
+ if errored_reports.any?
44
+ shell.newline
45
+ shell.error " * Errors:"
46
+ errored_reports.each do |r|
47
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
52
48
 
53
- shell.error " * #{r.name}"
54
- shell.error " #{display_metadata}" unless display_metadata.empty?
55
- r.errors.each do |error|
56
- shell.error " - #{error}"
57
- end
58
- shell.newline
49
+ shell.error " * #{r.name}"
50
+ shell.error " #{display_metadata}" unless display_metadata.empty?
51
+ r.errors.each do |error|
52
+ shell.error " - #{error}"
59
53
  end
60
- else
61
- shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
54
+ shell.newline
62
55
  end
63
-
64
- result
56
+ else
57
+ shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
65
58
  end
66
59
  end
67
60
 
68
- # Reports on a dependency in a cache command run.
69
- # Shows whether the dependency's record was cached or reused.
61
+ # Reports whether the dependency's record was cached or reused.
70
62
  #
71
63
  # dependency - An application dependency
72
- #
73
- # Returns the result of the yielded method
74
- # Note - must be called from inside the `report_run` scope
75
- def report_dependency(dependency)
76
- super do |report|
77
- result = yield report
78
-
79
- if report.errors.any?
80
- shell.error " Error #{dependency.name} (#{dependency.version})"
81
- elsif report["cached"]
82
- shell.info " Caching #{dependency.name} (#{dependency.version})"
83
- else
84
- shell.info " Using #{dependency.name} (#{dependency.version})"
85
- end
86
-
87
- result
64
+ # report - A report containing information about the dependency evaluation
65
+ def end_report_dependency(dependency, report)
66
+ if report.errors.any?
67
+ shell.error " Error #{dependency.name} (#{dependency.version})"
68
+ elsif report["cached"]
69
+ shell.info " Caching #{dependency.name} (#{dependency.version})"
70
+ else
71
+ shell.info " Using #{dependency.name} (#{dependency.version})"
88
72
  end
89
73
  end
90
74
  end
@@ -4,31 +4,29 @@ require "json"
4
4
  module Licensed
5
5
  module Reporters
6
6
  class JsonReporter < Reporter
7
- def report_run(command)
8
- super do |report|
9
- result = yield report
10
-
11
- report["apps"] = report.reports.map(&:to_h) if report.reports.any?
12
- shell.info JSON.pretty_generate(report.to_h)
13
-
14
- result
15
- end
7
+ # Report all information from the command run to the shell as a JSON object
8
+ #
9
+ # command - The command being run
10
+ # report - A report object containing information about the command run
11
+ def end_report_command(command, report)
12
+ report["apps"] = report.reports.map(&:to_h) if report.reports.any?
13
+ shell.info JSON.pretty_generate(report.to_h)
16
14
  end
17
15
 
18
- def report_app(app)
19
- super do |report|
20
- result = yield report
21
- report["sources"] = report.reports.map(&:to_h) if report.reports.any?
22
- result
23
- end
16
+ # Add source report information to the app report hash
17
+ #
18
+ # app - An application configuration
19
+ # report - A report object containing information about the app evaluation
20
+ def end_report_app(app, report)
21
+ report["sources"] = report.reports.map(&:to_h) if report.reports.any?
24
22
  end
25
23
 
26
- def report_source(source)
27
- super do |report|
28
- result = yield report
29
- report["dependencies"] = report.reports.map(&:to_h) if report.reports.any?
30
- result
31
- end
24
+ # Add dependency report information to the source report hash
25
+ #
26
+ # source - A dependency source enumerator
27
+ # report - A report object containing information about the source evaluation
28
+ def end_report_source(source, report)
29
+ report["dependencies"] = report.reports.map(&:to_h) if report.reports.any?
32
30
  end
33
31
  end
34
32
  end
@@ -3,84 +3,71 @@
3
3
  module Licensed
4
4
  module Reporters
5
5
  class ListReporter < Reporter
6
- # Reports on an application configuration in a list command run
6
+ # Reports the start of application configuration in a list command run
7
7
  #
8
8
  # app - An application configuration
9
- #
10
- # Returns the result of the yielded method
11
- # Note - must be called from inside the `report_run` scope
12
- def report_app(app)
13
- super do |report|
14
- shell.info "Listing dependencies for #{app["name"]}"
15
- yield report
16
- end
9
+ # report - A report object containing information about the app evaluation
10
+ def begin_report_app(app, report)
11
+ shell.info "Listing dependencies for #{app["name"]}"
17
12
  end
18
13
 
19
- # Reports on a dependency source enumerator in a list command run.
20
- # Shows the type and count of dependencies found by the source.
14
+ # Reports the start of a source evaluation
21
15
  #
22
16
  # source - A dependency source enumerator
23
- #
24
- # Returns the result of the yielded method
25
- # Note - must be called from inside the `report_run` scope
26
- def report_source(source)
27
- super do |report|
28
- shell.info " #{source.class.type}"
29
- result = yield report
17
+ # report - A report object containing information about the source evaluation
18
+ def begin_report_source(source, report)
19
+ shell.info " #{source.class.type}"
20
+ end
30
21
 
31
- warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
32
- if warning_reports.any?
33
- shell.newline
34
- shell.warn " * Warnings:"
35
- warning_reports.each do |r|
36
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
22
+ # Report the type and count of dependencies found by the source,
23
+ # along with any warnings and errors
24
+ #
25
+ # source - A dependency source enumerator
26
+ # report - A report object containing information about the source evaluation
27
+ def end_report_source(source, report)
28
+ warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
29
+ if warning_reports.any?
30
+ shell.newline
31
+ shell.warn " * Warnings:"
32
+ warning_reports.each do |r|
33
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
37
34
 
38
- shell.warn " * #{r.name}"
39
- shell.warn " #{display_metadata}" unless display_metadata.empty?
40
- r.warnings.each do |warning|
41
- shell.warn " - #{warning}"
42
- end
43
- shell.newline
35
+ shell.warn " * #{r.name}"
36
+ shell.warn " #{display_metadata}" unless display_metadata.empty?
37
+ r.warnings.each do |warning|
38
+ shell.warn " - #{warning}"
44
39
  end
40
+ shell.newline
45
41
  end
42
+ end
46
43
 
47
- errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
48
- if errored_reports.any?
49
- shell.newline
50
- shell.error " * Errors:"
51
- errored_reports.each do |r|
52
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
44
+ errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
45
+ if errored_reports.any?
46
+ shell.newline
47
+ shell.error " * Errors:"
48
+ errored_reports.each do |r|
49
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
53
50
 
54
- shell.error " * #{r.name}"
55
- shell.error " #{display_metadata}" unless display_metadata.empty?
56
- r.errors.each do |error|
57
- shell.error " - #{error}"
58
- end
59
- shell.newline
51
+ shell.error " * #{r.name}"
52
+ shell.error " #{display_metadata}" unless display_metadata.empty?
53
+ r.errors.each do |error|
54
+ shell.error " - #{error}"
60
55
  end
61
- else
62
- shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
56
+ shell.newline
63
57
  end
64
-
65
- result
58
+ else
59
+ shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
66
60
  end
67
61
  end
68
62
 
69
63
  # Reports on a dependency in a list command run.
70
64
  #
71
65
  # dependency - An application dependency
72
- #
73
- # Returns the result of the yielded method
74
- # Note - must be called from inside the `report_run` scope
75
- def report_dependency(dependency)
76
- super do |report|
77
- result = yield report
78
- info = "#{dependency.name} (#{dependency.version})"
79
- info = "#{info}: #{report["license"]}" if report["license"]
80
- shell.info " #{info}"
81
-
82
- result
83
- end
66
+ # report - A report object containing information about the dependency evaluation
67
+ def end_report_dependency(dependency, report)
68
+ info = "#{dependency.name} (#{dependency.version})"
69
+ info = "#{info}: #{report["license"]}" if report["license"]
70
+ shell.info " #{info}"
84
71
  end
85
72
  end
86
73
  end