licensed 1.5.2 → 2.0.0

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