licensed 2.15.2 → 3.2.0

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