licensed 1.5.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +22 -1
- data/CONTRIBUTING.md +2 -2
- data/README.md +17 -24
- data/Rakefile +2 -2
- data/docs/adding_a_new_source.md +93 -0
- data/docs/commands.md +81 -0
- data/docs/configuration.md +8 -8
- data/docs/migrating_to_newer_versions.md +3 -0
- data/docs/reporters.md +174 -0
- data/docs/sources/bundler.md +5 -5
- data/lib/licensed.rb +5 -14
- data/lib/licensed/cli.rb +23 -9
- data/lib/licensed/commands.rb +9 -0
- data/lib/licensed/commands/cache.rb +82 -0
- data/lib/licensed/commands/command.rb +112 -0
- data/lib/licensed/commands/list.rb +24 -0
- data/lib/licensed/commands/status.rb +49 -0
- data/lib/licensed/configuration.rb +3 -8
- data/lib/licensed/dependency.rb +116 -58
- data/lib/licensed/dependency_record.rb +76 -0
- data/lib/licensed/migrations.rb +7 -0
- data/lib/licensed/migrations/v2.rb +65 -0
- data/lib/licensed/reporters.rb +9 -0
- data/lib/licensed/reporters/cache_reporter.rb +76 -0
- data/lib/licensed/reporters/list_reporter.rb +69 -0
- data/lib/licensed/reporters/reporter.rb +119 -0
- data/lib/licensed/reporters/status_reporter.rb +67 -0
- data/lib/licensed/shell.rb +8 -10
- data/lib/licensed/sources.rb +15 -0
- data/lib/licensed/{source → sources}/bower.rb +14 -19
- data/lib/licensed/{source → sources}/bundler.rb +73 -48
- data/lib/licensed/{source → sources}/cabal.rb +40 -46
- data/lib/licensed/{source → sources}/dep.rb +15 -27
- data/lib/licensed/{source → sources}/git_submodule.rb +14 -19
- data/lib/licensed/{source → sources}/go.rb +28 -35
- data/lib/licensed/{source → sources}/manifest.rb +68 -90
- data/lib/licensed/{source → sources}/npm.rb +16 -25
- data/lib/licensed/{source → sources}/pip.rb +23 -25
- data/lib/licensed/sources/source.rb +69 -0
- data/lib/licensed/ui/shell.rb +4 -0
- data/lib/licensed/version.rb +6 -1
- data/licensed.gemspec +4 -4
- data/script/source-setup/bundler +1 -1
- metadata +32 -18
- data/lib/licensed/command/cache.rb +0 -82
- data/lib/licensed/command/list.rb +0 -43
- data/lib/licensed/command/status.rb +0 -79
- data/lib/licensed/command/version.rb +0 -18
- data/lib/licensed/license.rb +0 -68
data/lib/licensed/shell.rb
CHANGED
@@ -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.
|
7
|
-
# it raises an exception that contains the error output, unless
|
8
|
-
# `allow_failure` is true
|
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
|
-
@
|
39
|
+
@stderr = stderr.to_s.strip
|
41
40
|
end
|
42
41
|
|
43
42
|
def message
|
44
|
-
|
45
|
-
|
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
|
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
|
22
|
-
|
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(
|
26
|
-
"
|
27
|
-
"
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
9
|
-
class Bundler
|
10
|
-
|
11
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
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
|
26
|
-
|
49
|
+
def enumerate_dependencies
|
50
|
+
with_local_configuration do
|
27
51
|
specs.map do |spec|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
#
|
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) ||
|
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.
|
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
|
141
|
-
|
142
|
-
|
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("
|
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("
|
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
|
-
|
214
|
-
|
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
|
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
|
23
|
-
|
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(
|
28
|
-
"
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
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
|
93
|
-
package_dependencies_command(id
|
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
|
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
|
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
|
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
|
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
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
"
|
33
|
-
|
34
|
-
|
35
|
-
"version" => package[:version]
|
36
|
-
})
|
37
|
-
end
|
23
|
+
"homepage" => "https://#{package[:name]}"
|
24
|
+
}
|
25
|
+
)
|
38
26
|
end
|
39
27
|
end
|
40
28
|
|