licensed 3.0.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) 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 +180 -47
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +60 -1
  7. data/README.md +25 -79
  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 -161
  29. data/docs/sources/swift.md +4 -0
  30. data/lib/licensed/cli.rb +2 -2
  31. data/lib/licensed/commands/cache.rb +19 -20
  32. data/lib/licensed/commands/command.rb +104 -72
  33. data/lib/licensed/commands/environment.rb +12 -11
  34. data/lib/licensed/commands/list.rb +0 -19
  35. data/lib/licensed/commands/notices.rb +0 -19
  36. data/lib/licensed/commands/status.rb +13 -15
  37. data/lib/licensed/configuration.rb +105 -12
  38. data/lib/licensed/report.rb +44 -0
  39. data/lib/licensed/reporters/cache_reporter.rb +48 -64
  40. data/lib/licensed/reporters/json_reporter.rb +19 -21
  41. data/lib/licensed/reporters/list_reporter.rb +45 -58
  42. data/lib/licensed/reporters/notices_reporter.rb +33 -46
  43. data/lib/licensed/reporters/reporter.rb +37 -104
  44. data/lib/licensed/reporters/status_reporter.rb +58 -56
  45. data/lib/licensed/reporters/yaml_reporter.rb +19 -21
  46. data/lib/licensed/sources/bundler/definition.rb +36 -0
  47. data/lib/licensed/sources/bundler/missing_specification.rb +1 -1
  48. data/lib/licensed/sources/bundler.rb +38 -86
  49. data/lib/licensed/sources/dep.rb +2 -2
  50. data/lib/licensed/sources/go.rb +3 -3
  51. data/lib/licensed/sources/gradle.rb +2 -2
  52. data/lib/licensed/sources/helpers/content_versioning.rb +2 -1
  53. data/lib/licensed/sources/npm.rb +4 -3
  54. data/lib/licensed/sources/nuget.rb +56 -27
  55. data/lib/licensed/sources/swift.rb +69 -0
  56. data/lib/licensed/sources.rb +1 -0
  57. data/lib/licensed/version.rb +1 -1
  58. data/lib/licensed.rb +1 -0
  59. data/licensed.gemspec +4 -4
  60. data/script/source-setup/go +1 -1
  61. data/script/source-setup/swift +22 -0
  62. metadata +48 -13
  63. data/docs/commands.md +0 -95
@@ -3,80 +3,82 @@
3
3
  module Licensed
4
4
  module Reporters
5
5
  class StatusReporter < Reporter
6
- # Generate a report for a licensed status command run
7
- # Shows the errors found when checking status, as well as
8
- # overall number of dependencies checked
6
+ # Reports any errors encountered at the command level
9
7
  #
10
- # Returns the result of the yielded method
11
- def report_app(app)
12
- super do |report|
13
- shell.info "Checking cached dependency records for #{app["name"]}"
8
+ # command - The command being run
9
+ # report - A report object containing information about the command run
10
+ def end_report_command(command, report)
11
+ if report.errors.any?
12
+ shell.newline
13
+ report.errors.each { |e| shell.error e }
14
+ end
15
+ end
14
16
 
15
- result = yield report
17
+ # Reports the start of checking records for an app
18
+ #
19
+ # app - An application configuration
20
+ # report - A report containing information about the app evaluation
21
+ def begin_report_app(app, report)
22
+ shell.info "Checking cached dependency records for #{app["name"]}"
23
+ end
16
24
 
17
- all_reports = report.all_reports
25
+ # Reports any errors found when checking status, as well as
26
+ # overall number of dependencies checked
27
+ #
28
+ # app - An application configuration
29
+ # report - A report containing information about the app evaluation
30
+ def end_report_app(app, report)
31
+ all_reports = report.all_reports
18
32
 
19
- warning_reports = all_reports.select { |r| r.warnings.any? }.to_a
20
- if warning_reports.any?
21
- shell.newline
22
- shell.warn "Warnings:"
23
- warning_reports.each do |r|
24
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
33
+ warning_reports = all_reports.select { |r| r.warnings.any? }.to_a
34
+ if warning_reports.any?
35
+ shell.newline
36
+ shell.warn "Warnings:"
37
+ warning_reports.each do |r|
38
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
25
39
 
26
- shell.warn "* #{r.name}"
27
- shell.warn " #{display_metadata}" unless display_metadata.empty?
28
- r.warnings.each do |warning|
29
- shell.warn " - #{warning}"
30
- end
31
- shell.newline
40
+ shell.warn "* #{r.name}"
41
+ shell.warn " #{display_metadata}" unless display_metadata.empty?
42
+ r.warnings.each do |warning|
43
+ shell.warn " - #{warning}"
32
44
  end
45
+ shell.newline
33
46
  end
47
+ end
34
48
 
35
- errored_reports = all_reports.select { |r| r.errors.any? }.to_a
49
+ errored_reports = all_reports.select { |r| r.errors.any? }.to_a
36
50
 
37
- dependency_count = all_reports.select { |r| r.target.is_a?(Licensed::Dependency) }.size
38
- error_count = errored_reports.sum { |r| r.errors.size }
51
+ dependency_count = all_reports.count { |r| r.target.is_a?(Licensed::Dependency) }
52
+ error_count = errored_reports.sum { |r| r.errors.size }
39
53
 
40
- if error_count > 0
41
- shell.newline
42
- shell.error "Errors:"
43
- errored_reports.each do |r|
44
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
54
+ if error_count > 0
55
+ shell.newline
56
+ shell.error "Errors:"
57
+ errored_reports.each do |r|
58
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
45
59
 
46
- shell.error "* #{r.name}"
47
- shell.error " #{display_metadata}" unless display_metadata.empty?
48
- r.errors.each do |error|
49
- shell.error " - #{error}"
50
- end
51
- shell.newline
60
+ shell.error "* #{r.name}"
61
+ shell.error " #{display_metadata}" unless display_metadata.empty?
62
+ r.errors.each do |error|
63
+ shell.error " - #{error}"
52
64
  end
65
+ shell.newline
53
66
  end
54
-
55
- shell.newline
56
- shell.info "#{dependency_count} dependencies checked, #{error_count} errors found."
57
-
58
- result
59
67
  end
68
+
69
+ shell.newline
70
+ shell.info "#{dependency_count} dependencies checked, #{error_count} errors found."
60
71
  end
61
72
 
62
- # Reports on a dependency in a status command run.
63
- # Shows whether the dependency's status is valid in dot format
73
+ # Reports whether the dependency's status is valid in dot format
64
74
  #
65
75
  # dependency - An application dependency
66
- #
67
- # Returns the result of the yielded method
68
- # Note - must be called from inside the `report_run` scope
69
- def report_dependency(dependency)
70
- super do |report|
71
- result = yield report
72
-
73
- if report.errors.empty?
74
- shell.confirm(".", false)
75
- else
76
- shell.error("F", false)
77
- end
78
-
79
- result
76
+ # report - A report containing information about the dependency evaluation
77
+ def end_report_dependency(dependency, report)
78
+ if report.errors.empty?
79
+ shell.confirm(".", false)
80
+ else
81
+ shell.error("F", false)
80
82
  end
81
83
  end
82
84
  end
@@ -2,31 +2,29 @@
2
2
  module Licensed
3
3
  module Reporters
4
4
  class YamlReporter < Reporter
5
- def report_run(command)
6
- super do |report|
7
- result = yield report
8
-
9
- report["apps"] = report.reports.map(&:to_h) if report.reports.any?
10
- shell.info sanitize(report.to_h).to_yaml
11
-
12
- result
13
- end
5
+ # Report all information from the command run to the shell as a YAML object
6
+ #
7
+ # command - The command being run
8
+ # report - A report object containing information about the command run
9
+ def end_report_command(command, report)
10
+ report["apps"] = report.reports.map(&:to_h) if report.reports.any?
11
+ shell.info sanitize(report.to_h).to_yaml
14
12
  end
15
13
 
16
- def report_app(app)
17
- super do |report|
18
- result = yield report
19
- report["sources"] = report.reports.map(&:to_h) if report.reports.any?
20
- result
21
- end
14
+ # Add source report information to the app report hash
15
+ #
16
+ # app - An application configuration
17
+ # report - A report object containing information about the app evaluation
18
+ def end_report_app(app, report)
19
+ report["sources"] = report.reports.map(&:to_h) if report.reports.any?
22
20
  end
23
21
 
24
- def report_source(source)
25
- super do |report|
26
- result = yield report
27
- report["dependencies"] = report.reports.map(&:to_h) if report.reports.any?
28
- result
29
- end
22
+ # Add dependency report information to the source report hash
23
+ #
24
+ # source - A dependency source enumerator
25
+ # report - A report object containing information about the source evaluation
26
+ def end_report_source(source, report)
27
+ report["dependencies"] = report.reports.map(&:to_h) if report.reports.any?
30
28
  end
31
29
 
32
30
  def sanitize(object)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ module Bundler
5
+ module DefinitionExtensions
6
+ attr_accessor :force_exclude_groups
7
+
8
+ # Override specs to avoid logic that would raise Gem::NotFound
9
+ # which is handled in this ./missing_specification.rb, and to not add
10
+ # bundler as a dependency if it's not a user-requested gem.
11
+ #
12
+ # Newer versions of Bundler have changed the implementation of specs_for
13
+ # as well which no longer calls this function. Overriding this function
14
+ # gives a stable access point for licensed
15
+ def specs
16
+ @specs ||= begin
17
+ specs = resolve.materialize(requested_dependencies)
18
+
19
+ all_dependencies = requested_dependencies.concat(specs.flat_map(&:dependencies))
20
+ if all_dependencies.any? { |d| d.name == "bundler" } && !specs["bundler"].any?
21
+ bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", ::Bundler::VERSION)).last
22
+ specs["bundler"] = bundler
23
+ end
24
+
25
+ specs
26
+ end
27
+ end
28
+
29
+ # Override requested_groups to also exclude any groups that are
30
+ # in the "bundler.without" section of the licensed configuration file.
31
+ def requested_groups
32
+ super - Array(force_exclude_groups)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -48,7 +48,7 @@ module Bundler
48
48
  spec = orig_materialize
49
49
  return spec if spec
50
50
 
51
- Licensed::Bundler:: MissingSpecification.new(name: name, version: version, platform: platform, source: source)
51
+ Licensed::Bundler::MissingSpecification.new(name: name, version: version, platform: platform, source: source)
52
52
  end
53
53
  end
54
54
  end
@@ -3,6 +3,7 @@ require "delegate"
3
3
  begin
4
4
  require "bundler"
5
5
  require "licensed/sources/bundler/missing_specification"
6
+ require "licensed/sources/bundler/definition"
6
7
  rescue LoadError
7
8
  end
8
9
 
@@ -12,9 +13,9 @@ module Licensed
12
13
  class Dependency < Licensed::Dependency
13
14
  attr_reader :loaded_from
14
15
 
15
- def initialize(name:, version:, path:, loaded_from:, search_root:, errors: [], metadata: {})
16
+ def initialize(name:, version:, path:, loaded_from:, errors: [], metadata: {})
16
17
  @loaded_from = loaded_from
17
- super name: name, version: version, path: path, errors: errors, metadata: metadata, search_root: search_root
18
+ super name: name, version: version, path: path, errors: errors, metadata: metadata
18
19
  end
19
20
 
20
21
  # Load a package manager file from the base Licensee::Projects::FsProject
@@ -29,7 +30,7 @@ module Licensed
29
30
  # `loaded_from` if available.
30
31
  def spec_file
31
32
  return @spec_file if defined?(@spec_file)
32
- return @spec_file = nil unless loaded_from && File.exist?(loaded_from)
33
+ return @spec_file = nil unless loaded_from && File.file?(loaded_from)
33
34
  @spec_file = begin
34
35
  file = { name: File.basename(loaded_from), dir: File.dirname(loaded_from) }
35
36
  Licensee::ProjectFiles::PackageManagerFile.new(File.read(loaded_from), file)
@@ -37,7 +38,6 @@ module Licensed
37
38
  end
38
39
  end
39
40
 
40
- GEMFILES = { "Gemfile" => "Gemfile.lock", "gems.rb" => "gems.locked" }
41
41
  DEFAULT_WITHOUT_GROUPS = %i{development test}
42
42
  RUBY_PACKER_ERROR = "The bundler source cannot be used from the executable built with ruby-packer. Please install licensed using `gem install` or using bundler."
43
43
 
@@ -45,24 +45,28 @@ module Licensed
45
45
  # running a ruby-packer-built licensed exe when ruby isn't available
46
46
  # could lead to errors if the host ruby doesn't exist
47
47
  return false if ruby_packer? && !Licensed::Shell.tool_available?("ruby")
48
- defined?(::Bundler) && lockfile_path && lockfile_path.exist?
48
+
49
+ # if Bundler isn't loaded, this enumerator won't work!
50
+ return false unless defined?(::Bundler)
51
+
52
+ with_application_environment { ::Bundler.default_lockfile&.exist? }
53
+ rescue ::Bundler::GemfileNotFound
54
+ false
49
55
  end
50
56
 
51
57
  def enumerate_dependencies
52
58
  raise Licensed::Sources::Source::Error.new(RUBY_PACKER_ERROR) if ruby_packer?
53
59
 
54
- with_local_configuration do
55
- specs.map do |spec|
56
- next if spec.name == "bundler" && !include_bundler?
60
+ with_application_environment do
61
+ definition.specs.map do |spec|
57
62
  next if spec.name == config["name"]
58
63
 
59
64
  error = spec.error if spec.respond_to?(:error)
60
65
  Dependency.new(
61
66
  name: spec.name,
62
67
  version: spec.version.to_s,
63
- path: spec.gem_dir,
68
+ path: spec.full_gem_path,
64
69
  loaded_from: spec.loaded_from,
65
- search_root: spec_root(spec),
66
70
  errors: Array(error),
67
71
  metadata: {
68
72
  "type" => Bundler.type,
@@ -74,53 +78,13 @@ module Licensed
74
78
  end
75
79
  end
76
80
 
77
- # Returns an array of Gem::Specifications for all gem dependencies
78
- def specs
79
- @specs ||= definition.specs_for(groups)
80
- end
81
-
82
- # Returns whether to include bundler as a listed dependency of the project
83
- def include_bundler?
84
- @include_bundler ||= begin
85
- # include if bundler is listed as a direct dependency that should be included
86
- requested_dependencies = definition.dependencies.select { |d| (d.groups & groups).any? && d.should_include? }
87
- return true if requested_dependencies.any? { |d| d.name == "bundler" }
88
- # include if bundler is an indirect dependency
89
- return true if specs.flat_map(&:dependencies).any? { |d| d.name == "bundler" }
90
- false
91
- end
92
- end
93
-
94
- # Returns a search root for a specification, one of:
95
- # - the local bundler gem location
96
- # - the system rubygems install gem location
97
- # - nil
98
- def spec_root(spec)
99
- return if spec.gem_dir.nil?
100
- root = [Gem.default_dir, Gem.dir].find { |dir| spec.gem_dir.start_with?(dir) }
101
- return unless root
102
-
103
- "#{root}/gems/#{spec.full_name}"
104
- end
105
-
106
- # Build the bundler definition
107
81
  def definition
108
- @definition ||= ::Bundler::Definition.build(gemfile_path, lockfile_path, nil)
109
- end
110
-
111
- # Returns the bundle definition groups, removing "without" groups,
112
- # and including "with" groups
113
- def groups
114
- @groups ||= definition.groups - bundler_setting_array(:without) + bundler_setting_array(:with) - exclude_groups
115
- end
116
-
117
- # Returns a bundler setting as an array.
118
- # Depending on the version of bundler, array values are either returned as
119
- # a raw string ("a:b:c") or as an array ([:a, :b, :c])
120
- def bundler_setting_array(key)
121
- setting = ::Bundler.settings[key]
122
- setting = setting.split(":").map(&:to_sym) if setting.is_a?(String)
123
- Array(setting)
82
+ @definition ||= begin
83
+ definition = ::Bundler::Definition.build(::Bundler.default_gemfile, ::Bundler.default_lockfile, nil)
84
+ definition.extend Licensed::Bundler::DefinitionExtensions
85
+ definition.force_exclude_groups = exclude_groups
86
+ definition
87
+ end
124
88
  end
125
89
 
126
90
  # Returns any groups to exclude specified from both licensed configuration
@@ -134,41 +98,29 @@ module Licensed
134
98
  end
135
99
  end
136
100
 
137
- # Returns the path to the Bundler Gemfile
138
- def gemfile_path
139
- @gemfile_path ||= GEMFILES.keys
140
- .map { |g| config.pwd.join g }
141
- .find { |f| f.exist? }
142
- end
143
-
144
- # Returns the path to the Bundler Gemfile.lock
145
- def lockfile_path
146
- return unless gemfile_path
147
- @lockfile_path ||= gemfile_path.dirname.join(GEMFILES[gemfile_path.basename.to_s])
148
- end
149
-
150
- private
151
-
152
101
  # helper to clear all bundler environment around a yielded block
153
- def with_local_configuration
154
- # force bundler to use the local gem file
155
- original_bundle_gemfile, ENV["BUNDLE_GEMFILE"] = ENV["BUNDLE_GEMFILE"], gemfile_path.to_s
102
+ def with_application_environment
103
+ backup = nil
156
104
 
157
- # silence any bundler warnings while running licensed
158
- bundler_ui, ::Bundler.ui = ::Bundler.ui, ::Bundler::UI::Silent.new
105
+ ::Bundler.ui.silence do
106
+ if ::Bundler.root != config.source_path
107
+ backup = ENV.to_hash
108
+ ENV.replace(::Bundler.original_env)
159
109
 
160
- # reset all bundler configuration
161
- ::Bundler.reset!
162
- # and re-configure with settings for current directory
163
- ::Bundler.configure
110
+ # reset bundler to load from the current app's source path
111
+ ::Bundler.reset!
112
+ ::Bundler.load
113
+ end
164
114
 
165
- yield
115
+ yield
116
+ end
166
117
  ensure
167
- ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
168
- ::Bundler.ui = bundler_ui
169
-
170
- # restore bundler configuration
171
- ::Bundler.configure
118
+ if backup
119
+ # restore bundler configuration
120
+ ENV.replace(backup)
121
+ ::Bundler.reset!
122
+ ::Bundler.load
123
+ end
172
124
  end
173
125
 
174
126
  # Returns whether the current licensed execution is running ruby-packer
@@ -40,10 +40,10 @@ module Licensed
40
40
  end
41
41
  end
42
42
 
43
- # Returns the godoc.org page for a package.
43
+ # Returns the pkg.go.dev page for a package.
44
44
  def homepage(import_path)
45
45
  return unless import_path
46
- "https://godoc.org/#{import_path}"
46
+ "https://pkg.go.dev/#{import_path}"
47
47
  end
48
48
 
49
49
  # Returns whether the package is part of the go std list. Replaces
@@ -98,7 +98,7 @@ module Licensed
98
98
  # Returns whether the package is local to the current project
99
99
  def local_package?(package)
100
100
  return false unless package && package["Dir"]
101
- return false unless File.fnmatch?("#{config.root.to_s}*", package["Dir"])
101
+ return false unless File.fnmatch?("#{config.root}*", package["Dir"], File::FNM_CASEFOLD)
102
102
  vendored_path_parts(package).nil?
103
103
  end
104
104
 
@@ -132,10 +132,10 @@ module Licensed
132
132
  end
133
133
  end
134
134
 
135
- # Returns the godoc.org page for a package.
135
+ # Returns the pkg.go.dev page for a package.
136
136
  def homepage(import_path)
137
137
  return unless import_path
138
- "https://godoc.org/#{import_path}"
138
+ "https://pkg.go.dev/#{import_path}"
139
139
  end
140
140
 
141
141
  # Returns the root directory to search for a package license