licensed 2.15.2 → 3.2.0

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +55 -11
  3. data/CHANGELOG.md +56 -1
  4. data/README.md +38 -81
  5. data/docs/adding_a_new_source.md +11 -8
  6. data/docs/commands/README.md +59 -0
  7. data/docs/commands/cache.md +35 -0
  8. data/docs/commands/env.md +10 -0
  9. data/docs/commands/list.md +23 -0
  10. data/docs/commands/migrate.md +10 -0
  11. data/docs/commands/notices.md +12 -0
  12. data/docs/commands/status.md +73 -0
  13. data/docs/commands/version.md +3 -0
  14. data/docs/configuration.md +9 -161
  15. data/docs/configuration/README.md +11 -0
  16. data/docs/configuration/allowed_licenses.md +17 -0
  17. data/docs/configuration/application_name.md +63 -0
  18. data/docs/configuration/application_source.md +64 -0
  19. data/docs/configuration/configuration_root.md +27 -0
  20. data/docs/configuration/configuring_multiple_apps.md +58 -0
  21. data/docs/configuration/dependency_source_enumerators.md +28 -0
  22. data/docs/configuration/ignoring_dependencies.md +19 -0
  23. data/docs/configuration/metadata_cache.md +106 -0
  24. data/docs/configuration/reviewing_dependencies.md +18 -0
  25. data/docs/{migrating_to_newer_versions.md → migrations/v2.md} +1 -1
  26. data/docs/migrations/v3.md +109 -0
  27. data/docs/sources/bundler.md +1 -11
  28. data/docs/sources/swift.md +4 -0
  29. data/lib/licensed.rb +1 -0
  30. data/lib/licensed/cli.rb +6 -3
  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.rb +1 -0
  47. data/lib/licensed/sources/bundler.rb +36 -217
  48. data/lib/licensed/sources/bundler/missing_specification.rb +54 -0
  49. data/lib/licensed/sources/go.rb +1 -1
  50. data/lib/licensed/sources/gradle.rb +2 -2
  51. data/lib/licensed/sources/npm.rb +4 -3
  52. data/lib/licensed/sources/nuget.rb +57 -27
  53. data/lib/licensed/sources/swift.rb +69 -0
  54. data/lib/licensed/version.rb +1 -1
  55. data/script/source-setup/go +1 -1
  56. data/script/source-setup/swift +22 -0
  57. metadata +27 -4
  58. data/docs/commands.md +0 -95
@@ -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)
@@ -14,6 +14,7 @@ module Licensed
14
14
  require "licensed/sources/nuget"
15
15
  require "licensed/sources/pip"
16
16
  require "licensed/sources/pipenv"
17
+ require "licensed/sources/swift"
17
18
  require "licensed/sources/gradle"
18
19
  require "licensed/sources/mix"
19
20
  require "licensed/sources/yarn"
@@ -2,50 +2,13 @@
2
2
  require "delegate"
3
3
  begin
4
4
  require "bundler"
5
+ require "licensed/sources/bundler/missing_specification"
5
6
  rescue LoadError
6
7
  end
7
8
 
8
9
  module Licensed
9
10
  module Sources
10
11
  class Bundler < Source
11
- class MissingSpecification < Gem::BasicSpecification
12
- attr_reader :name, :requirement
13
- alias_method :version, :requirement
14
- def initialize(name:, requirement:)
15
- @name = name
16
- @requirement = requirement
17
- end
18
-
19
- def dependencies
20
- []
21
- end
22
-
23
- def source
24
- nil
25
- end
26
-
27
- def platform; end
28
- def gem_dir; end
29
- def gems_dir
30
- Gem.dir
31
- end
32
- def summary; end
33
- def homepage; end
34
-
35
- def error
36
- "could not find #{name} (#{requirement}) in any sources"
37
- end
38
- end
39
-
40
- class BundlerSpecification < ::SimpleDelegator
41
- def gem_dir
42
- dir = super
43
- return dir if File.exist?(dir)
44
-
45
- File.join(Gem.dir, "gems", full_name)
46
- end
47
- end
48
-
49
12
  class Dependency < Licensed::Dependency
50
13
  attr_reader :loaded_from
51
14
 
@@ -66,7 +29,7 @@ module Licensed
66
29
  # `loaded_from` if available.
67
30
  def spec_file
68
31
  return @spec_file if defined?(@spec_file)
69
- return @spec_file = nil unless loaded_from && File.exist?(loaded_from)
32
+ return @spec_file = nil unless loaded_from && File.file?(loaded_from)
70
33
  @spec_file = begin
71
34
  file = { name: File.basename(loaded_from), dir: File.dirname(loaded_from) }
72
35
  Licensee::ProjectFiles::PackageManagerFile.new(File.read(loaded_from), file)
@@ -76,6 +39,7 @@ module Licensed
76
39
 
77
40
  GEMFILES = { "Gemfile" => "Gemfile.lock", "gems.rb" => "gems.locked" }
78
41
  DEFAULT_WITHOUT_GROUPS = %i{development test}
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."
79
43
 
80
44
  def enabled?
81
45
  # running a ruby-packer-built licensed exe when ruby isn't available
@@ -85,13 +49,18 @@ module Licensed
85
49
  end
86
50
 
87
51
  def enumerate_dependencies
52
+ raise Licensed::Sources::Source::Error.new(RUBY_PACKER_ERROR) if ruby_packer?
53
+
88
54
  with_local_configuration do
89
55
  specs.map do |spec|
56
+ next if spec.name == "bundler" && !include_bundler?
57
+ next if spec.name == config["name"]
58
+
90
59
  error = spec.error if spec.respond_to?(:error)
91
60
  Dependency.new(
92
61
  name: spec.name,
93
62
  version: spec.version.to_s,
94
- path: spec.gem_dir,
63
+ path: spec.full_gem_path,
95
64
  loaded_from: spec.loaded_from,
96
65
  errors: Array(error),
97
66
  metadata: {
@@ -106,136 +75,18 @@ module Licensed
106
75
 
107
76
  # Returns an array of Gem::Specifications for all gem dependencies
108
77
  def specs
109
- # get the specifications for all dependencies in a Gemfile
110
- root_dependencies = definition.dependencies.select { |d| include?(d, nil) }
111
- root_specs = specs_for_dependencies(root_dependencies, nil).compact
112
-
113
- # recursively find the remaining specifications
114
- all_specs = recursive_specs(root_specs)
115
-
116
- # delete any specifications loaded from a gemspec
117
- all_specs.delete_if { |s| s.source.is_a?(::Bundler::Source::Gemspec) }
118
- end
119
-
120
- # Recursively finds the dependencies for Gem specifications.
121
- # Returns a `Set` containing the package names for all dependencies
122
- def recursive_specs(specs, results = Set.new)
123
- return [] if specs.nil? || specs.empty?
124
-
125
- new_specs = Set.new(specs) - results.to_a
126
- return [] if new_specs.empty?
127
-
128
- results.merge new_specs
129
-
130
- dependency_specs = new_specs.flat_map { |s| specs_for_dependencies(s.dependencies, s.source) }
131
-
132
- return results if dependency_specs.empty?
133
-
134
- results.merge recursive_specs(dependency_specs, results)
135
- end
136
-
137
- # Returns the specs for dependencies that pass the checks in `include?`.
138
- # Returns a `MissingSpecification` if a gem specification isn't found.
139
- def specs_for_dependencies(dependencies, source)
140
- included_dependencies = dependencies.select { |d| include?(d, source) }
141
- included_dependencies.map do |dep|
142
- gem_spec(dep) || MissingSpecification.new(name: dep.name, requirement: dep.requirement)
143
- end
144
- end
145
-
146
- # Returns a Gem::Specification for the provided gem argument.
147
- def gem_spec(dependency)
148
- return unless dependency
149
-
150
- # find a specifiction from the resolved ::Bundler::Definition specs
151
- spec = definition.resolve.find { |s| s.satisfies?(dependency) }
152
-
153
- # a nil spec should be rare, generally only seen from bundler
154
- return matching_spec(dependency) || bundle_exec_gem_spec(dependency.name, dependency.requirement) if spec.nil?
155
-
156
- # try to find a non-lazy specification that matches `spec`
157
- # spec.source.specs gives access to specifications with more
158
- # information than spec itself, including platform-specific gems.
159
- # these objects should have all the information needed to detect license metadata
160
- source_spec = spec.source.specs.find { |s| s.name == spec.name && s.version == spec.version }
161
- return source_spec if source_spec
162
-
163
- # look for a specification at the bundler specs path
164
- spec_path = ::Bundler.specs_path.join("#{spec.full_name}.gemspec")
165
- return Gem::Specification.load(spec_path.to_s) if File.exist?(spec_path.to_s)
166
-
167
- # if the specification file doesn't exist, get the specification using
168
- # the bundler and gem CLI
169
- bundle_exec_gem_spec(dependency.name, dependency.requirement)
170
- end
171
-
172
- # Returns whether a dependency should be included in the final
173
- def include?(dependency, source)
174
- # ::Bundler::Dependency has an extra `should_include?`
175
- return false unless dependency.should_include? if dependency.respond_to?(:should_include?)
176
-
177
- # Don't return gems added from `add_development_dependency` in a gemspec
178
- # if the :development group is excluded
179
- gemspec_source = source.is_a?(::Bundler::Source::Gemspec)
180
- return false if dependency.type == :development && (!gemspec_source || exclude_development_dependencies?)
181
-
182
- # Gem::Dependency don't have groups - in our usage these objects always
183
- # come as child-dependencies and are never directly from a Gemfile.
184
- # We assume that all Gem::Dependencies are ok at this point
185
- return true if dependency.groups.nil?
186
-
187
- # check if the dependency is in any groups we're interested in
188
- (dependency.groups & groups).any?
189
- end
190
-
191
- # Returns whether development dependencies should be excluded
192
- def exclude_development_dependencies?
193
- @include_development ||= begin
194
- # check whether the development dependency group is explicitly removed
195
- # or added via bundler and licensed configurations
196
- groups = [:development] - Array(::Bundler.settings[:without]) + Array(::Bundler.settings[:with]) - exclude_groups
197
- !groups.include?(:development)
198
- end
199
- end
200
-
201
- # Load a gem specification from the YAML returned from `gem specification`
202
- # This is a last resort when licensed can't obtain a specification from other means
203
- def bundle_exec_gem_spec(name, requirement)
204
- # `gem` must be available to run `gem specification`
205
- return unless Licensed::Shell.tool_available?("gem")
206
-
207
- # use `gem specification` with a clean ENV and clean Gem.dir paths
208
- # to get gem specification at the right directory
209
- begin
210
- ::Bundler.with_original_env do
211
- ::Bundler.rubygems.clear_paths
212
- yaml = Licensed::Shell.execute(*ruby_command_args("gem", "specification", name, "-v", requirement.to_s))
213
- spec = Gem::Specification.from_yaml(yaml)
214
- # this is horrible, but it will cache the gem_dir using the clean env
215
- # so that it can be used outside of this block when running from
216
- # the ruby packer executable environment
217
- spec.gem_dir if ruby_packer?
218
- spec
219
- end
220
- rescue Licensed::Shell::Error
221
- # return nil
222
- ensure
223
- ::Bundler.configure
224
- end
78
+ @specs ||= definition.specs_for(groups)
225
79
  end
226
80
 
227
- # Loads a dependency specification using rubygems' built-in
228
- # `Dependency#matching_specs` and `Dependency#to_spec`, from the original
229
- # gem environment
230
- def matching_spec(dependency)
231
- begin
232
- ::Bundler.with_original_env do
233
- ::Bundler.rubygems.clear_paths
234
- return unless dependency.matching_specs(true).any?
235
- BundlerSpecification.new(dependency.to_spec)
236
- end
237
- ensure
238
- ::Bundler.configure
81
+ # Returns whether to include bundler as a listed dependency of the project
82
+ def include_bundler?
83
+ @include_bundler ||= begin
84
+ # include if bundler is listed as a direct dependency that should be included
85
+ requested_dependencies = definition.dependencies.select { |d| (d.groups & groups).any? && d.should_include? }
86
+ return true if requested_dependencies.any? { |d| d.name == "bundler" }
87
+ # include if bundler is an indirect dependency
88
+ return true if specs.flat_map(&:dependencies).any? { |d| d.name == "bundler" }
89
+ false
239
90
  end
240
91
  end
241
92
 
@@ -283,71 +134,39 @@ module Licensed
283
134
  @lockfile_path ||= gemfile_path.dirname.join(GEMFILES[gemfile_path.basename.to_s])
284
135
  end
285
136
 
286
- # Returns the configured bundler executable to use, or "bundle" by default.
287
- def bundler_exe
288
- @bundler_exe ||= begin
289
- exe = config.dig("bundler", "bundler_exe")
290
- return "bundle" unless exe
291
- return exe if Licensed::Shell.tool_available?(exe)
292
- config.root.join(exe)
293
- end
294
- end
295
-
296
- # Determines if the configured bundler executable is available and returns
297
- # shell command args with or without `bundle exec` depending on availability.
298
- def ruby_command_args(*args)
299
- return Array(args) unless Licensed::Shell.tool_available?(bundler_exe)
300
- [bundler_exe, "exec", *args]
301
- end
302
-
303
- private
304
-
305
137
  # helper to clear all bundler environment around a yielded block
306
138
  def with_local_configuration
307
- # force bundler to use the local gem file
308
- original_bundle_gemfile, ENV["BUNDLE_GEMFILE"] = ENV["BUNDLE_GEMFILE"], gemfile_path.to_s
139
+ # silence any bundler warnings while running licensed
140
+ bundler_ui, ::Bundler.ui = ::Bundler.ui, ::Bundler::UI::Silent.new
309
141
 
310
- if ruby_packer?
311
- # if running under ruby-packer, set environment from host
142
+ original_bundle_gemfile = nil
143
+ if gemfile_path.to_s != ENV["BUNDLE_GEMFILE"]
144
+ # force bundler to use the local gem file
145
+ original_bundle_gemfile, ENV["BUNDLE_GEMFILE"] = ENV["BUNDLE_GEMFILE"], gemfile_path.to_s
312
146
 
313
- # hack: setting this ENV var allows licensed to use Gem paths outside
314
- # of the ruby-packer filesystem. this is needed to find spec sources
315
- # from the host filesystem
316
- ENV["ENCLOSE_IO_RUBYC_1ST_PASS"] = "1"
317
- ruby_version = Gem::ConfigMap[:ruby_version]
318
- # set the ruby version in Gem::ConfigMap to the ruby version from the host.
319
- # this helps Bundler find the correct spec sources and paths
320
- Gem::ConfigMap[:ruby_version] = host_ruby_version
147
+ # reset all bundler configuration
148
+ ::Bundler.reset!
149
+ # and re-configure with settings for current directory
150
+ ::Bundler.configure
321
151
  end
322
152
 
323
- # reset all bundler configuration
324
- ::Bundler.reset!
325
- # and re-configure with settings for current directory
326
- ::Bundler.configure
327
-
328
153
  yield
329
154
  ensure
330
- if ruby_packer?
331
- # if running under ruby-packer, restore environment after block is finished
332
- ENV.delete("ENCLOSE_IO_RUBYC_1ST_PASS")
333
- Gem::ConfigMap[:ruby_version] = ruby_version
155
+ if original_bundle_gemfile
156
+ ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
157
+
158
+ # restore bundler configuration
159
+ ::Bundler.reset!
160
+ ::Bundler.configure
334
161
  end
335
162
 
336
- ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
337
- # restore bundler configuration
338
- ::Bundler.reset!
339
- ::Bundler.configure
163
+ ::Bundler.ui = bundler_ui
340
164
  end
341
165
 
342
166
  # Returns whether the current licensed execution is running ruby-packer
343
167
  def ruby_packer?
344
168
  @ruby_packer ||= RbConfig::TOPDIR =~ /__enclose_io_memfs__/
345
169
  end
346
-
347
- # Returns the ruby version found in the bundler environment
348
- def host_ruby_version
349
- Licensed::Shell.execute(*ruby_command_args("ruby", "-e", "puts Gem::ConfigMap[:ruby_version]"))
350
- end
351
170
  end
352
171
  end
353
172
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/match_platform"
4
+
5
+ # Bundler normally raises a "GemNotFound" error when a specification
6
+ # can't be materialized which halts bundler dependency enumeration.
7
+
8
+ # This monkey patch instead creates MissingSpecification objects to
9
+ # identify missing specs without raising errors and halting enumeration.
10
+ # It was the most minimal-touch solution I could think of that should reliably
11
+ # work across many bundler versions
12
+
13
+ module Licensed
14
+ module Bundler
15
+ class MissingSpecification < Gem::BasicSpecification
16
+ include ::Bundler::MatchPlatform
17
+
18
+ attr_reader :name, :version, :platform, :source
19
+ def initialize(name:, version:, platform:, source:)
20
+ @name = name
21
+ @version = version
22
+ @platform = platform
23
+ @source = source
24
+ end
25
+
26
+ def dependencies
27
+ []
28
+ end
29
+
30
+ def gem_dir; end
31
+ def gems_dir
32
+ Gem.dir
33
+ end
34
+ def summary; end
35
+ def homepage; end
36
+
37
+ def error
38
+ "could not find #{name} (#{version}) in any sources"
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ module Bundler
45
+ class LazySpecification
46
+ alias_method :orig_materialize, :__materialize__
47
+ def __materialize__
48
+ spec = orig_materialize
49
+ return spec if spec
50
+
51
+ Licensed::Bundler::MissingSpecification.new(name: name, version: version, platform: platform, source: source)
52
+ end
53
+ end
54
+ end
@@ -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.to_s}*", package["Dir"], File::FNM_CASEFOLD)
102
102
  vendored_path_parts(package).nil?
103
103
  end
104
104
 
@@ -125,10 +125,10 @@ module Licensed
125
125
  def self.add_gradle_license_report_plugins_block(gradle_build_file)
126
126
 
127
127
  if gradle_build_file.include? "plugins"
128
- gradle_build_file.gsub(/(?<=plugins)\s+{/, " { id 'com.github.jk1.dependency-license-report' version '1.6'")
128
+ gradle_build_file.gsub(/(?<=plugins)\s+{/, " { id 'com.github.jk1.dependency-license-report' version '1.16'")
129
129
  else
130
130
 
131
- gradle_build_file = " plugins { id 'com.github.jk1.dependency-license-report' version '1.6' }" + gradle_build_file
131
+ gradle_build_file = " plugins { id 'com.github.jk1.dependency-license-report' version '1.16' }" + gradle_build_file
132
132
  end
133
133
  end
134
134