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.
- 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
|
|