licensed 3.1.0 → 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
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