licensed 1.5.2 → 2.0.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +22 -1
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +17 -24
  6. data/Rakefile +2 -2
  7. data/docs/adding_a_new_source.md +93 -0
  8. data/docs/commands.md +81 -0
  9. data/docs/configuration.md +8 -8
  10. data/docs/migrating_to_newer_versions.md +3 -0
  11. data/docs/reporters.md +174 -0
  12. data/docs/sources/bundler.md +5 -5
  13. data/lib/licensed.rb +5 -14
  14. data/lib/licensed/cli.rb +23 -9
  15. data/lib/licensed/commands.rb +9 -0
  16. data/lib/licensed/commands/cache.rb +82 -0
  17. data/lib/licensed/commands/command.rb +112 -0
  18. data/lib/licensed/commands/list.rb +24 -0
  19. data/lib/licensed/commands/status.rb +49 -0
  20. data/lib/licensed/configuration.rb +3 -8
  21. data/lib/licensed/dependency.rb +116 -58
  22. data/lib/licensed/dependency_record.rb +76 -0
  23. data/lib/licensed/migrations.rb +7 -0
  24. data/lib/licensed/migrations/v2.rb +65 -0
  25. data/lib/licensed/reporters.rb +9 -0
  26. data/lib/licensed/reporters/cache_reporter.rb +76 -0
  27. data/lib/licensed/reporters/list_reporter.rb +69 -0
  28. data/lib/licensed/reporters/reporter.rb +119 -0
  29. data/lib/licensed/reporters/status_reporter.rb +67 -0
  30. data/lib/licensed/shell.rb +8 -10
  31. data/lib/licensed/sources.rb +15 -0
  32. data/lib/licensed/{source → sources}/bower.rb +14 -19
  33. data/lib/licensed/{source → sources}/bundler.rb +73 -48
  34. data/lib/licensed/{source → sources}/cabal.rb +40 -46
  35. data/lib/licensed/{source → sources}/dep.rb +15 -27
  36. data/lib/licensed/{source → sources}/git_submodule.rb +14 -19
  37. data/lib/licensed/{source → sources}/go.rb +28 -35
  38. data/lib/licensed/{source → sources}/manifest.rb +68 -90
  39. data/lib/licensed/{source → sources}/npm.rb +16 -25
  40. data/lib/licensed/{source → sources}/pip.rb +23 -25
  41. data/lib/licensed/sources/source.rb +69 -0
  42. data/lib/licensed/ui/shell.rb +4 -0
  43. data/lib/licensed/version.rb +6 -1
  44. data/licensed.gemspec +4 -4
  45. data/script/source-setup/bundler +1 -1
  46. metadata +32 -18
  47. data/lib/licensed/command/cache.rb +0 -82
  48. data/lib/licensed/command/list.rb +0 -43
  49. data/lib/licensed/command/status.rb +0 -79
  50. data/lib/licensed/command/version.rb +0 -18
  51. data/lib/licensed/license.rb +0 -68
@@ -3,16 +3,14 @@ require "open3"
3
3
 
4
4
  module Licensed
5
5
  module Shell
6
- # Executes a command, returning its standard output on success. On failure,
7
- # it raises an exception that contains the error output, unless
8
- # `allow_failure` is true, in which case it returns an empty string.
6
+ # Executes a command, returning its standard output on success.
7
+ # On failure it raises an exception that contains the error output, unless
8
+ # `allow_failure` is true.
9
9
  def self.execute(cmd, *args, allow_failure: false, env: {})
10
10
  stdout, stderr, status = Open3.capture3(env, cmd, *args)
11
11
 
12
- if status.success?
12
+ if status.success? || allow_failure
13
13
  stdout.strip
14
- elsif allow_failure
15
- ""
16
14
  else
17
15
  raise Error.new([cmd, *args], status.exitstatus, stderr)
18
16
  end
@@ -33,17 +31,17 @@ module Licensed
33
31
  end
34
32
 
35
33
  class Error < RuntimeError
34
+ attr_reader :cmd, :status, :stderr
36
35
  def initialize(cmd, status, stderr)
37
36
  super()
38
37
  @cmd = cmd
39
38
  @exitstatus = status
40
- @output = stderr
39
+ @stderr = stderr.to_s.strip
41
40
  end
42
41
 
43
42
  def message
44
- output = @output.to_s.strip
45
- extra = output.empty?? "" : "\n#{output.gsub(/^/, " ")}"
46
- "command exited with status #{@exitstatus}\n #{escape_cmd}#{extra}"
43
+ extra = @stderr.empty?? "" : "#{@stderr.gsub(/^/, " ")}"
44
+ "'#{escape_cmd}' exited with status #{@exitstatus}\n#{extra}"
47
45
  end
48
46
 
49
47
  def escape_cmd
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module Licensed
3
+ module Sources
4
+ require "licensed/sources/source"
5
+ require "licensed/sources/bower"
6
+ require "licensed/sources/bundler"
7
+ require "licensed/sources/cabal"
8
+ require "licensed/sources/dep"
9
+ require "licensed/sources/git_submodule"
10
+ require "licensed/sources/go"
11
+ require "licensed/sources/manifest"
12
+ require "licensed/sources/npm"
13
+ require "licensed/sources/pip"
14
+ end
15
+ end
@@ -2,33 +2,28 @@
2
2
  require "json"
3
3
 
4
4
  module Licensed
5
- module Source
6
- class Bower
7
- def self.type
8
- "bower"
9
- end
10
-
11
- def initialize(config)
12
- @config = config
13
- end
14
-
5
+ module Sources
6
+ class Bower < Source
15
7
  def enabled?
16
8
  [@config.pwd.join(".bowerrc"), @config.pwd.join("bower.json")].any? do |path|
17
9
  File.exist?(path)
18
10
  end
19
11
  end
20
12
 
21
- def dependencies
22
- @dependencies ||= Dir.glob(bower_path.join("*/.bower.json")).map do |file|
13
+ def enumerate_dependencies
14
+ Dir.glob(bower_path.join("*/.bower.json")).map do |file|
23
15
  package = JSON.parse(File.read(file))
24
16
  path = bower_path.join(file).dirname.to_path
25
- Dependency.new(path, {
26
- "type" => Bower.type,
27
- "name" => package["name"],
28
- "version" => package["version"] || package["_release"],
29
- "summary" => package["description"],
30
- "homepage" => package["homepage"]
31
- })
17
+ Dependency.new(
18
+ name: package["name"],
19
+ version: package["version"] || package["_release"],
20
+ path: path,
21
+ metadata: {
22
+ "type" => Bower.type,
23
+ "summary" => package["description"],
24
+ "homepage" => package["homepage"]
25
+ }
26
+ )
32
27
  end
33
28
  end
34
29
 
@@ -5,33 +5,62 @@ rescue LoadError
5
5
  end
6
6
 
7
7
  module Licensed
8
- module Source
9
- class Bundler
10
- GEMFILES = %w{Gemfile gems.rb}.freeze
11
- DEFAULT_WITHOUT_GROUPS = %i{development test}
8
+ module Sources
9
+ class Bundler < Source
10
+ class MissingSpecification < Gem::BasicSpecification
11
+ attr_reader :name, :requirement
12
+ alias_method :version, :requirement
13
+ def initialize(name:, requirement:)
14
+ @name = name
15
+ @requirement = requirement
16
+ end
12
17
 
13
- def self.type
14
- "rubygem"
15
- end
18
+ def dependencies
19
+ []
20
+ end
21
+
22
+ def source
23
+ nil
24
+ end
25
+
26
+ def platform; end
27
+ def gem_dir; end
28
+ def gems_dir
29
+ Gem.dir
30
+ end
31
+ def summary; end
32
+ def homepage; end
16
33
 
17
- def initialize(config)
18
- @config = config
34
+ def error
35
+ "could not find #{name} (#{requirement}) in any sources"
36
+ end
19
37
  end
20
38
 
39
+ GEMFILES = %w{Gemfile gems.rb}.freeze
40
+ DEFAULT_WITHOUT_GROUPS = %i{development test}
41
+
21
42
  def enabled?
43
+ # running a ruby-packer-built licensed exe when ruby isn't available
44
+ # could lead to errors if the host ruby doesn't exist
45
+ return false if ruby_packer? && !Licensed::Shell.tool_available?("ruby")
22
46
  defined?(::Bundler) && lockfile_path && lockfile_path.exist?
23
47
  end
24
48
 
25
- def dependencies
26
- @dependencies ||= with_local_configuration do
49
+ def enumerate_dependencies
50
+ with_local_configuration do
27
51
  specs.map do |spec|
28
- Licensed::Dependency.new(spec.gem_dir, {
29
- "type" => Bundler.type,
30
- "name" => spec.name,
31
- "version" => spec.version.to_s,
32
- "summary" => spec.summary,
33
- "homepage" => spec.homepage
34
- })
52
+ error = spec.error if spec.respond_to?(:error)
53
+ Licensed::Dependency.new(
54
+ name: spec.name,
55
+ version: spec.version.to_s,
56
+ path: spec.gem_dir,
57
+ errors: Array(error),
58
+ metadata: {
59
+ "type" => Bundler.type,
60
+ "summary" => spec.summary,
61
+ "homepage" => spec.homepage
62
+ }
63
+ )
35
64
  end
36
65
  end
37
66
  end
@@ -66,17 +95,16 @@ module Licensed
66
95
  results.merge recursive_specs(dependency_specs, results)
67
96
  end
68
97
 
69
- # Returns the specs for dependencies that pass the checks in `include?`
70
- # Raises an error if the specification isn't found
98
+ # Returns the specs for dependencies that pass the checks in `include?`.
99
+ # Returns a `MissingSpecification` if a gem specification isn't found.
71
100
  def specs_for_dependencies(dependencies, source)
72
101
  included_dependencies = dependencies.select { |d| include?(d, source) }
73
102
  included_dependencies.map do |dep|
74
- gem_spec(dep) || raise("Unable to find a specification for #{dep.name} (#{dep.requirement}) in any sources")
103
+ gem_spec(dep) || MissingSpecification.new(name: dep.name, requirement: dep.requirement)
75
104
  end
76
105
  end
77
106
 
78
- # Returns a Gem::Specification for the provided gem argument. If a
79
- # Gem::Specification isn't found, an error will be raised.
107
+ # Returns a Gem::Specification for the provided gem argument.
80
108
  def gem_spec(dependency)
81
109
  return unless dependency
82
110
 
@@ -137,9 +165,23 @@ module Licensed
137
165
  # `gem` must be available to run `gem specification`
138
166
  return unless Licensed::Shell.tool_available?("gem")
139
167
 
140
- # use `gem specification` with a clean ENV to get gem specification YAML
141
- yaml = ::Bundler.with_original_env { Licensed::Shell.execute(*ruby_command_args("gem", "specification", name)) }
142
- Gem::Specification.from_yaml(yaml)
168
+ # use `gem specification` with a clean ENV and clean Gem.dir paths
169
+ # to get gem specification at the right directory
170
+ begin
171
+ ::Bundler.with_original_env do
172
+ ::Bundler.rubygems.clear_paths
173
+ yaml = Licensed::Shell.execute(*ruby_command_args("gem", "specification", name))
174
+ spec = Gem::Specification.from_yaml(yaml)
175
+ # this is horrible, but it will cache the gem_dir using the clean env
176
+ # so that it can be used outside of this block
177
+ spec.gem_dir
178
+ spec
179
+ end
180
+ rescue Licensed::Shell::Error
181
+ # return nil
182
+ ensure
183
+ ::Bundler.configure
184
+ end
143
185
  end
144
186
 
145
187
  # Build the bundler definition
@@ -158,8 +200,7 @@ module Licensed
158
200
  # Defaults to [:development, :test] + ::Bundler.settings[:without]
159
201
  def exclude_groups
160
202
  @exclude_groups ||= begin
161
- exclude = Array(@config.dig("rubygem", "without"))
162
- exclude = Array(@config.dig("rubygems", "without")) if exclude.empty? # :sad:
203
+ exclude = Array(@config.dig("bundler", "without"))
163
204
  exclude = DEFAULT_WITHOUT_GROUPS if exclude.empty?
164
205
  exclude.uniq.map(&:to_sym)
165
206
  end
@@ -180,7 +221,7 @@ module Licensed
180
221
  # Returns the configured bundler executable to use, or "bundle" by default.
181
222
  def bundler_exe
182
223
  @bundler_exe ||= begin
183
- exe = @config.dig("rubygem", "bundler_exe")
224
+ exe = @config.dig("bundler", "bundler_exe")
184
225
  return "bundle" unless exe
185
226
  return exe if Licensed::Shell.tool_available?(exe)
186
227
  @config.root.join(exe)
@@ -209,25 +250,9 @@ module Licensed
209
250
  # from the host filesystem
210
251
  ENV["ENCLOSE_IO_RUBYC_1ST_PASS"] = "1"
211
252
  ruby_version = Gem::ConfigMap[:ruby_version]
212
-
213
- if Licensed::Shell.tool_available?("ruby")
214
- # set the ruby version in Gem::ConfigMap to the ruby version from the host.
215
- # this helps Bundler find the correct spec sources and paths
216
- Gem::ConfigMap[:ruby_version] = host_ruby_version
217
- else
218
- # running a ruby-packer-built licensed exe when ruby and bundler aren't available
219
- # is possible but could lead to errors if the host ruby version doesn't
220
- # match the built executable's ruby version
221
- @config.ui.warn <<~WARNING
222
- Ruby wasn't found when enumerating bundler
223
- dependencies using the licensed executable. This can cause a
224
- ruby mismatch between licensed and bundled dependencies and a
225
- failure to find gem specifications.
226
-
227
- If licensed is unable to find gem specifications that you believe are present,
228
- please ensure that ruby and bundler are available and try again.
229
- WARNING
230
- end
253
+ # set the ruby version in Gem::ConfigMap to the ruby version from the host.
254
+ # this helps Bundler find the correct spec sources and paths
255
+ Gem::ConfigMap[:ruby_version] = host_ruby_version
231
256
  end
232
257
 
233
258
  # reset all bundler configuration
@@ -2,39 +2,50 @@
2
2
  require "English"
3
3
 
4
4
  module Licensed
5
- module Source
6
- class Cabal
5
+ module Sources
6
+ class Cabal < Source
7
7
  DEPENDENCY_REGEX = /\s*.+?\s*/.freeze
8
8
  DEFAULT_TARGETS = %w{executable library}.freeze
9
9
 
10
- def self.type
11
- "cabal"
12
- end
13
-
14
- def initialize(config)
15
- @config = config
16
- end
17
-
18
10
  def enabled?
19
11
  cabal_file_dependencies.any? && ghc?
20
12
  end
21
13
 
22
- def dependencies
23
- @dependencies ||= package_ids.map do |id|
24
- package = package_info(id)
25
-
14
+ def enumerate_dependencies
15
+ packages.map do |package|
26
16
  path, search_root = package_docs_dirs(package)
27
- Dependency.new(path, {
28
- "type" => Cabal.type,
29
- "name" => package["name"],
30
- "version" => package["version"],
31
- "summary" => package["synopsis"],
32
- "homepage" => safe_homepage(package["homepage"]),
33
- "search_root" => search_root
34
- })
17
+ Dependency.new(
18
+ name: package["name"],
19
+ version: package["version"],
20
+ path: path,
21
+ search_root: search_root,
22
+ errors: Array(package["error"]),
23
+ metadata: {
24
+ "type" => Cabal.type,
25
+ "summary" => package["synopsis"],
26
+ "homepage" => safe_homepage(package["homepage"])
27
+ }
28
+ )
35
29
  end
36
30
  end
37
31
 
32
+ # Returns a list of all detected packages
33
+ def packages
34
+ missing = []
35
+ package_ids = Set.new
36
+ cabal_file_dependencies.each do |target|
37
+ name, version = target.split(/\s/, 2)
38
+ package_id = cabal_package_id(name)
39
+ if package_id.nil?
40
+ missing << { "name" => name, "version" => version, "error" => "package not found" }
41
+ else
42
+ recursive_dependencies([package_id], package_ids)
43
+ end
44
+ end
45
+
46
+ package_ids.map { |id| package_info(id) }.concat(missing)
47
+ end
48
+
38
49
  # Returns the packages document directory and search root directory
39
50
  # as an array
40
51
  def package_docs_dirs(package)
@@ -64,23 +75,17 @@ module Licensed
64
75
  .gsub(/#[^?]*\z/, "")
65
76
  end
66
77
 
67
- # Returns a `Set` of the package ids for all cabal dependencies
68
- def package_ids
69
- recursive_dependencies(cabal_file_dependency_ids)
70
- end
71
-
72
78
  # Recursively finds the dependencies for each cabal package.
73
79
  # Returns a `Set` containing the package names for all dependencies
74
80
  def recursive_dependencies(package_names, results = Set.new)
75
81
  return [] if package_names.nil? || package_names.empty?
76
82
 
77
- new_packages = Set.new(package_names) - results.to_a
83
+ new_packages = Set.new(package_names) - results
78
84
  return [] if new_packages.empty?
79
85
 
80
86
  results.merge new_packages
81
87
 
82
88
  dependencies = new_packages.flat_map { |n| package_dependencies(n) }
83
- .compact
84
89
 
85
90
  return results if dependencies.empty?
86
91
 
@@ -89,23 +94,16 @@ module Licensed
89
94
 
90
95
  # Returns an array of dependency package names for the cabal package
91
96
  # given by `id`
92
- def package_dependencies(id, full_id = true)
93
- package_dependencies_command(id, full_id).gsub("depends:", "")
94
- .split
95
- .map(&:strip)
97
+ def package_dependencies(id)
98
+ package_dependencies_command(id).gsub("depends:", "").split.map(&:strip)
96
99
  end
97
100
 
98
101
  # Returns the output of running `ghc-pkg field depends` for a package id
99
102
  # Optionally allows for interpreting the given id as an
100
103
  # installed package id (`--ipid`)
101
- def package_dependencies_command(id, full_id)
104
+ def package_dependencies_command(id)
102
105
  fields = %w(depends)
103
-
104
- if full_id
105
- ghc_pkg_field_command(id, fields, "--ipid")
106
- else
107
- ghc_pkg_field_command(id, fields)
108
- end
106
+ ghc_pkg_field_command(id, fields, "--ipid")
109
107
  end
110
108
 
111
109
  # Returns package information as a hash for the given id
@@ -150,11 +148,7 @@ module Licensed
150
148
  path.gsub("<ghc_version>", ghc_version)
151
149
  end
152
150
 
153
- # Returns a set containing the top-level dependencies found in cabal files
154
- def cabal_file_dependency_ids
155
- cabal_file_dependencies.map { |target| cabal_package_id(target) }.compact
156
- end
157
-
151
+ # Returns a set of the top-level dependencies found in cabal files
158
152
  def cabal_file_dependencies
159
153
  @cabal_file_dependencies ||= cabal_files.each_with_object(Set.new) do |cabal_file, targets|
160
154
  content = File.read(cabal_file)
@@ -166,7 +160,7 @@ module Licensed
166
160
  # match[1] is a string of "," separated dependencies.
167
161
  # dependency packages might have a version specifier, remove them
168
162
  # to get the full id specifier for each package
169
- dependencies = match[1].split(",").map { |dep| dep.strip.split(/\s/)[0] }
163
+ dependencies = match[1].split(",").map(&:strip)
170
164
  targets.merge(dependencies)
171
165
  end
172
166
  end
@@ -2,39 +2,27 @@
2
2
  require "tomlrb"
3
3
 
4
4
  module Licensed
5
- module Source
6
- class Dep
7
- def self.type
8
- "dep"
9
- end
10
-
11
- def initialize(config)
12
- @config = config
13
- end
14
-
5
+ module Sources
6
+ class Dep < Source
15
7
  def enabled?
16
8
  go_dep_available?
17
9
  end
18
10
 
19
- def dependencies
20
- @dependencies ||= begin
21
- packages.map do |package|
22
- package_dir = @config.pwd.join("vendor", package[:name])
23
- search_root = @config.pwd.join("vendor", package[:project])
24
-
25
- unless package_dir.exist?
26
- next if @config.ignored?("type" => Dep.type, "name" => package[:name])
27
- raise "couldn't find package for #{package[:name]}"
28
- end
11
+ def enumerate_dependencies
12
+ packages.map do |package|
13
+ package_dir = @config.pwd.join("vendor", package[:name])
14
+ search_root = @config.pwd.join("vendor", package[:project])
29
15
 
30
- Dependency.new(package_dir.to_s, {
16
+ Dependency.new(
17
+ name: package[:name],
18
+ version: package[:version],
19
+ path: package_dir.to_s,
20
+ search_root: search_root.to_s,
21
+ metadata: {
31
22
  "type" => Dep.type,
32
- "name" => package[:name],
33
- "homepage" => "https://#{package[:name]}",
34
- "search_root" => search_root.to_s,
35
- "version" => package[:version]
36
- })
37
- end
23
+ "homepage" => "https://#{package[:name]}"
24
+ }
25
+ )
38
26
  end
39
27
  end
40
28