inspec 0.29.0 → 0.30.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -2
- data/Rakefile +53 -0
- data/docs/cli.rst +442 -0
- data/examples/inheritance/inspec.yml +3 -0
- data/inspec.gemspec +1 -0
- data/lib/inspec/cli.rb +10 -1
- data/lib/inspec/completions/bash.sh.erb +45 -0
- data/lib/inspec/completions/zsh.sh.erb +61 -0
- data/lib/inspec/dependencies.rb +307 -0
- data/lib/inspec/dsl.rb +5 -20
- data/lib/inspec/env_printer.rb +149 -0
- data/lib/inspec/errors.rb +17 -0
- data/lib/inspec/metadata.rb +4 -0
- data/lib/inspec/profile.rb +12 -0
- data/lib/inspec/profile_context.rb +5 -2
- data/lib/inspec/shell.rb +7 -2
- data/lib/inspec/shell_detector.rb +90 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/postgres.rb +94 -12
- data/lib/resources/registry_key.rb +106 -27
- data/lib/utils/hash_map.rb +37 -0
- data/test/bench/startup.flat.txt +998 -0
- data/test/bench/startup.graph.html +71420 -0
- data/test/bench/startup.grind.dat +103554 -0
- data/test/bench/startup.stack.html +25015 -0
- data/test/bench/startup/startup.flat.txt +1005 -0
- data/test/bench/startup/startup.graph.html +71958 -0
- data/test/bench/startup/startup.grind.dat +101602 -0
- data/test/bench/startup/startup.stack.html +24516 -0
- data/test/cookbooks/os_prepare/metadata.rb +1 -0
- data/test/cookbooks/os_prepare/recipes/file.rb +5 -0
- data/test/cookbooks/os_prepare/recipes/registry_key.rb +13 -0
- data/test/docker_run.rb +3 -1
- data/test/functional/inheritance_test.rb +26 -13
- data/test/helper.rb +2 -2
- data/test/integration/default/file_spec.rb +16 -0
- data/test/integration/default/powershell_spec.rb +4 -1
- data/test/integration/default/registry_key_spec.rb +47 -4
- data/test/integration/default/secpol_spec.rb +4 -1
- data/test/integration/default/wmi_spec.rb +4 -1
- data/test/unit/mock/profiles/resource-tiny/inspec.yml +10 -0
- data/test/unit/mock/profiles/resource-tiny/libraries/resource.rb +3 -0
- data/test/unit/shell_detector_test.rb +78 -0
- metadata +47 -4
- data/docs/ctl_inspec.rst +0 -247
data/inspec.gemspec
CHANGED
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_dependency 'rspec-its', '~> 1.2'
|
35
35
|
spec.add_dependency 'pry', '~> 0'
|
36
36
|
spec.add_dependency 'hashie', '~> 3.4'
|
37
|
+
spec.add_dependency 'molinillo', '~> 0.5'
|
37
38
|
|
38
39
|
spec.add_development_dependency 'mocha', '~> 1.1'
|
39
40
|
end
|
data/lib/inspec/cli.rb
CHANGED
@@ -4,12 +4,15 @@
|
|
4
4
|
# author: Dominik Richter
|
5
5
|
# author: Christoph Hartmann
|
6
6
|
|
7
|
+
require 'logger'
|
7
8
|
require 'thor'
|
8
9
|
require 'json'
|
9
10
|
require 'pp'
|
10
11
|
require 'utils/json_log'
|
11
12
|
require 'inspec/base_cli'
|
13
|
+
require 'inspec/plugins'
|
12
14
|
require 'inspec/runner_mock'
|
15
|
+
require 'inspec/env_printer'
|
13
16
|
|
14
17
|
class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
|
15
18
|
class_option :diagnose, type: :boolean,
|
@@ -152,7 +155,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
|
|
152
155
|
desc 'shell', 'open an interactive debugging shell'
|
153
156
|
target_options
|
154
157
|
option :command, aliases: :c
|
155
|
-
option :format, type: :string, default:
|
158
|
+
option :format, type: :string, default: nil, hide: true
|
156
159
|
def shell_func
|
157
160
|
diagnose
|
158
161
|
o = opts.dup
|
@@ -170,6 +173,12 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
|
|
170
173
|
$stderr.puts e.message
|
171
174
|
end
|
172
175
|
|
176
|
+
desc 'env', 'Output shell-appropriate completion configuration'
|
177
|
+
def env(shell = nil)
|
178
|
+
p = Inspec::EnvPrinter.new(self.class, shell)
|
179
|
+
p.print_and_exit!
|
180
|
+
end
|
181
|
+
|
173
182
|
desc 'version', 'prints the version of this tool'
|
174
183
|
def version
|
175
184
|
puts Inspec::VERSION
|
@@ -0,0 +1,45 @@
|
|
1
|
+
_inspec() {
|
2
|
+
local _inspec_top_level_commands="<%= top_level_commands.join(" ") %>"
|
3
|
+
<% subcommands_with_commands.each do |name, subcommands| -%>
|
4
|
+
local _inspec_<%= name %>_commands="<%= subcommands.join(" ") -%>"
|
5
|
+
<% end -%>
|
6
|
+
|
7
|
+
cur=${COMP_WORDS[COMP_CWORD]}
|
8
|
+
prev=${COMP_WORDS[COMP_CWORD-1]}
|
9
|
+
|
10
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
11
|
+
case "$prev" in
|
12
|
+
inspec)
|
13
|
+
COMPREPLY=( $( compgen -W "$_inspec_top_level_commands" -- "$cur" ) )
|
14
|
+
;;
|
15
|
+
esac
|
16
|
+
elif [ "$COMP_CWORD" -eq 2 ]; then
|
17
|
+
case "$prev" in
|
18
|
+
archive|check|exec|json)
|
19
|
+
COMPREPLY=( $( compgen -f -- "$cur" ) )
|
20
|
+
;;
|
21
|
+
help)
|
22
|
+
COMPREPLY=( $( compgen -W "$_inspec_top_level_commands" -- "$cur" ) )
|
23
|
+
;;
|
24
|
+
<% subcommands_with_commands.each do |name, subcommands| -%>
|
25
|
+
<%= name %>)
|
26
|
+
COMPREPLY=( $( compgen -W "$_inspec_<%= name %>_commands" -- "$cur" ) )
|
27
|
+
;;
|
28
|
+
<% end -%>
|
29
|
+
esac
|
30
|
+
elif [ "$COMP_CWORD" -eq 3 ]; then
|
31
|
+
prev2=${COMP_WORDS[COMP_CWORD-2]}
|
32
|
+
case "$prev2-$prev" in
|
33
|
+
compliance-upload)
|
34
|
+
COMPREPLY=( $( compgen -f -- "$cur" ) )
|
35
|
+
;;
|
36
|
+
<% subcommands_with_commands.each do |name, subcommands| -%>
|
37
|
+
<%= name %>-help)
|
38
|
+
COMPREPLY=( $( compgen -W "$_inspec_<%= name %>_commands" -- "$cur" ) )
|
39
|
+
;;
|
40
|
+
<% end -%>
|
41
|
+
esac
|
42
|
+
fi
|
43
|
+
}
|
44
|
+
|
45
|
+
complete -F _inspec inspec
|
@@ -0,0 +1,61 @@
|
|
1
|
+
function _inspec() {
|
2
|
+
local curcontext="$curcontext" state line
|
3
|
+
typeset -A opt_args
|
4
|
+
|
5
|
+
local -a _top_level_commands <%= subcommands_with_commands_and_descriptions.keys.map {|i| "_#{i}_commands" }.join(' ') %>
|
6
|
+
|
7
|
+
_top_level_commands=(
|
8
|
+
<%= top_level_commands_with_descriptions.map {|i| " "*8 + "\"#{i}\"" }. join("\n") %>
|
9
|
+
)
|
10
|
+
|
11
|
+
<% subcommands_with_commands_and_descriptions.each do |name, entry| -%>
|
12
|
+
_<%= name %>_commands=(
|
13
|
+
<%= entry.map {|i| " "*8 + "\"#{i}\"" }.join("\n") %>
|
14
|
+
)
|
15
|
+
|
16
|
+
<% end -%>
|
17
|
+
_arguments '1:::->toplevel' && return 0
|
18
|
+
_arguments '2:::->subcommand' && return 0
|
19
|
+
_arguments '3:::->subsubcommand' && return 0
|
20
|
+
|
21
|
+
#
|
22
|
+
# Are you thinking? "Jeez, whoever wrote this really doesn't get
|
23
|
+
# zsh's completion system?" If so, you are correct. However, I
|
24
|
+
# have goodnews! Pull requests are accepted!
|
25
|
+
#
|
26
|
+
case $state in
|
27
|
+
toplevel)
|
28
|
+
_describe -t commands "InSpec subcommands" _top_level_commands
|
29
|
+
;;
|
30
|
+
subcommand)
|
31
|
+
case "$words[2]" in
|
32
|
+
archive|check|exec|json)
|
33
|
+
_alternative 'files:filenames:_files'
|
34
|
+
;;
|
35
|
+
help)
|
36
|
+
_describe -t commands "InSpec subcommands" _top_level_commands
|
37
|
+
;;
|
38
|
+
<% subcommands_with_commands_and_descriptions.each do |name, entry| -%>
|
39
|
+
<%= name %>)
|
40
|
+
_describe -t <%= name %>_commands "InSpec <%= name -%> subcommands" _<%= name %>_commands
|
41
|
+
;;
|
42
|
+
<% end -%>
|
43
|
+
esac
|
44
|
+
;;
|
45
|
+
subsubcommand)
|
46
|
+
case "$words[2]-$words[3]" in
|
47
|
+
compliance-upload)
|
48
|
+
_alternative 'files:filenames:_files'
|
49
|
+
;;
|
50
|
+
<% subcommands_with_commands_and_descriptions.each do |name, entry| -%>
|
51
|
+
<%= name %>-help)
|
52
|
+
_describe -t <%= name %>_commands "InSpec <%= name %> subcommands" _<%= name %>_commands
|
53
|
+
;;
|
54
|
+
<% end -%>
|
55
|
+
esac
|
56
|
+
|
57
|
+
esac
|
58
|
+
|
59
|
+
}
|
60
|
+
|
61
|
+
compdef _inspec inspec
|
@@ -0,0 +1,307 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'logger'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'molinillo'
|
8
|
+
require 'inspec/errors'
|
9
|
+
|
10
|
+
module Inspec
|
11
|
+
class Resolver
|
12
|
+
def self.resolve(requirements, vendor_index, cwd, opts = {})
|
13
|
+
reqs = requirements.map do |req|
|
14
|
+
Requirement.from_metadata(req, cwd: cwd) ||
|
15
|
+
fail("Cannot initialize dependency: #{req}")
|
16
|
+
end
|
17
|
+
|
18
|
+
new(vendor_index, opts).resolve(reqs)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(vendor_index, opts = {})
|
22
|
+
@logger = opts[:logger] || Logger.new(nil)
|
23
|
+
@debug_mode = false # TODO: hardcoded for now, grab from options
|
24
|
+
|
25
|
+
@vendor_index = vendor_index
|
26
|
+
@resolver = Molinillo::Resolver.new(self, self)
|
27
|
+
@search_cache = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Resolve requirements.
|
31
|
+
#
|
32
|
+
# @param requirements [Array(Inspec::requirement)] Array of requirements
|
33
|
+
# @return [Array(String)] list of resolved dependency paths
|
34
|
+
def resolve(requirements)
|
35
|
+
requirements.each(&:pull)
|
36
|
+
@base_dep_graph = Molinillo::DependencyGraph.new
|
37
|
+
@dep_graph = @resolver.resolve(requirements, @base_dep_graph)
|
38
|
+
arr = @dep_graph.map(&:payload)
|
39
|
+
Hash[arr.map { |e| [e.name, e] }]
|
40
|
+
rescue Molinillo::VersionConflict => e
|
41
|
+
raise VersionConflict.new(e.conflicts.keys.uniq, e.message)
|
42
|
+
rescue Molinillo::CircularDependencyError => e
|
43
|
+
names = e.dependencies.sort_by(&:name).map { |d| "profile '#{d.name}'" }
|
44
|
+
raise CyclicDependencyError,
|
45
|
+
'Your profile has requirements that depend on each other, creating '\
|
46
|
+
"an infinite loop. Please remove #{names.count > 1 ? 'either ' : ''} "\
|
47
|
+
"#{names.join(' or ')} and try again."
|
48
|
+
end
|
49
|
+
|
50
|
+
# --------------------------------------------------------------------------
|
51
|
+
# SpecificationProvider
|
52
|
+
|
53
|
+
# Search for the specifications that match the given dependency.
|
54
|
+
# The specifications in the returned array will be considered in reverse
|
55
|
+
# order, so the latest version ought to be last.
|
56
|
+
# @note This method should be 'pure', i.e. the return value should depend
|
57
|
+
# only on the `dependency` parameter.
|
58
|
+
#
|
59
|
+
# @param [Object] dependency
|
60
|
+
# @return [Array<Object>] the specifications that satisfy the given
|
61
|
+
# `dependency`.
|
62
|
+
def search_for(dep)
|
63
|
+
unless dep.is_a?(Inspec::Requirement)
|
64
|
+
fail 'Internal error: Dependency resolver requires an Inspec::Requirement object for #search_for(dependency)'
|
65
|
+
end
|
66
|
+
@search_cache[dep] ||= uncached_search_for(dep)
|
67
|
+
end
|
68
|
+
|
69
|
+
def uncached_search_for(dep)
|
70
|
+
# pre-cached and specified dependencies
|
71
|
+
return [dep] unless dep.profile.nil?
|
72
|
+
|
73
|
+
results = @vendor_index.find(dep)
|
74
|
+
return [] unless results.any?
|
75
|
+
|
76
|
+
# TODO: load dep from vendor index
|
77
|
+
# vertex = @dep_graph.vertex_named(dep.name)
|
78
|
+
# locked_requirement = vertex.payload.requirement if vertex
|
79
|
+
fail NotImplementedError, "load dependency #{dep} from vendor index"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the dependencies of `specification`.
|
83
|
+
# @note This method should be 'pure', i.e. the return value should depend
|
84
|
+
# only on the `specification` parameter.
|
85
|
+
#
|
86
|
+
# @param [Object] specification
|
87
|
+
# @return [Array<Object>] the dependencies that are required by the given
|
88
|
+
# `specification`.
|
89
|
+
def dependencies_for(specification)
|
90
|
+
specification.profile.metadata.dependencies
|
91
|
+
end
|
92
|
+
|
93
|
+
# Determines whether the given `requirement` is satisfied by the given
|
94
|
+
# `spec`, in the context of the current `activated` dependency graph.
|
95
|
+
#
|
96
|
+
# @param [Object] requirement
|
97
|
+
# @param [DependencyGraph] activated the current dependency graph in the
|
98
|
+
# resolution process.
|
99
|
+
# @param [Object] spec
|
100
|
+
# @return [Boolean] whether `requirement` is satisfied by `spec` in the
|
101
|
+
# context of the current `activated` dependency graph.
|
102
|
+
def requirement_satisfied_by?(requirement, _activated, spec)
|
103
|
+
requirement.matches_spec?(spec) || spec.is_a?(Inspec::Profile)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the name for the given `dependency`.
|
107
|
+
# @note This method should be 'pure', i.e. the return value should depend
|
108
|
+
# only on the `dependency` parameter.
|
109
|
+
#
|
110
|
+
# @param [Object] dependency
|
111
|
+
# @return [String] the name for the given `dependency`.
|
112
|
+
def name_for(dependency)
|
113
|
+
unless dependency.is_a?(Inspec::Requirement)
|
114
|
+
fail 'Internal error: Dependency resolver requires an Inspec::Requirement object for #name_for(dependency)'
|
115
|
+
end
|
116
|
+
dependency.name
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [String] the name of the source of explicit dependencies, i.e.
|
120
|
+
# those passed to {Resolver#resolve} directly.
|
121
|
+
def name_for_explicit_dependency_source
|
122
|
+
'inspec.yml'
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [String] the name of the source of 'locked' dependencies, i.e.
|
126
|
+
# those passed to {Resolver#resolve} directly as the `base`
|
127
|
+
def name_for_locking_dependency_source
|
128
|
+
'inspec.lock'
|
129
|
+
end
|
130
|
+
|
131
|
+
# Sort dependencies so that the ones that are easiest to resolve are first.
|
132
|
+
# Easiest to resolve is (usually) defined by:
|
133
|
+
# 1) Is this dependency already activated?
|
134
|
+
# 2) How relaxed are the requirements?
|
135
|
+
# 3) Are there any conflicts for this dependency?
|
136
|
+
# 4) How many possibilities are there to satisfy this dependency?
|
137
|
+
#
|
138
|
+
# @param [Array<Object>] dependencies
|
139
|
+
# @param [DependencyGraph] activated the current dependency graph in the
|
140
|
+
# resolution process.
|
141
|
+
# @param [{String => Array<Conflict>}] conflicts
|
142
|
+
# @return [Array<Object>] a sorted copy of `dependencies`.
|
143
|
+
def sort_dependencies(dependencies, activated, conflicts)
|
144
|
+
dependencies.sort_by do |dependency|
|
145
|
+
name = name_for(dependency)
|
146
|
+
[
|
147
|
+
activated.vertex_named(name).payload ? 0 : 1,
|
148
|
+
# amount_constrained(dependency), # TODO
|
149
|
+
conflicts[name] ? 0 : 1,
|
150
|
+
# activated.vertex_named(name).payload ? 0 : search_for(dependency).count, # TODO
|
151
|
+
]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns whether this dependency, which has no possible matching
|
156
|
+
# specifications, can safely be ignored.
|
157
|
+
#
|
158
|
+
# @param [Object] dependency
|
159
|
+
# @return [Boolean] whether this dependency can safely be skipped.
|
160
|
+
def allow_missing?(_dependency)
|
161
|
+
# TODO
|
162
|
+
false
|
163
|
+
end
|
164
|
+
|
165
|
+
# --------------------------------------------------------------------------
|
166
|
+
# UI
|
167
|
+
|
168
|
+
include Molinillo::UI
|
169
|
+
|
170
|
+
# The {IO} object that should be used to print output. `STDOUT`, by default.
|
171
|
+
#
|
172
|
+
# @return [IO]
|
173
|
+
def output
|
174
|
+
self
|
175
|
+
end
|
176
|
+
|
177
|
+
def print(what = '')
|
178
|
+
@logger.info(what)
|
179
|
+
end
|
180
|
+
alias puts print
|
181
|
+
end
|
182
|
+
|
183
|
+
class Package
|
184
|
+
def initialize(path, version)
|
185
|
+
@path = path
|
186
|
+
@version = version
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class VendorIndex
|
191
|
+
attr_reader :list, :path
|
192
|
+
def initialize(path)
|
193
|
+
@path = path
|
194
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
195
|
+
@list = Dir[File.join(path, '*')].map { |x| load_path(x) }
|
196
|
+
end
|
197
|
+
|
198
|
+
def find(_dependency)
|
199
|
+
# TODO
|
200
|
+
fail NotImplementedError, '#find(dependency) on VendorIndex seeks implementation.'
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def load_path(_path)
|
206
|
+
# TODO
|
207
|
+
fail NotImplementedError, '#load_path(path) on VendorIndex wants to be implemented.'
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
class Requirement
|
212
|
+
attr_reader :name, :dep, :cwd, :opts
|
213
|
+
def initialize(name, dep, cwd, opts)
|
214
|
+
@name = name
|
215
|
+
@dep = Gem::Dependency.new(name, Gem::Requirement.new(Array(dep)), :runtime)
|
216
|
+
@opts = opts
|
217
|
+
@cwd = cwd
|
218
|
+
end
|
219
|
+
|
220
|
+
def matches_spec?(spec)
|
221
|
+
params = spec.profile.metadata.params
|
222
|
+
@dep.match?(params[:name], params[:version])
|
223
|
+
end
|
224
|
+
|
225
|
+
def pull
|
226
|
+
case
|
227
|
+
when @opts[:path] then pull_path(@opts[:path])
|
228
|
+
else
|
229
|
+
# TODO: should default to supermarket
|
230
|
+
fail 'You must specify the source of the dependency (for now...)'
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def path
|
235
|
+
@path || pull
|
236
|
+
end
|
237
|
+
|
238
|
+
def profile
|
239
|
+
return nil if path.nil?
|
240
|
+
@profile ||= Inspec::Profile.for_target(path, {})
|
241
|
+
end
|
242
|
+
|
243
|
+
def self.from_metadata(dep, opts)
|
244
|
+
fail 'Cannot load empty dependency.' if dep.nil? || dep.empty?
|
245
|
+
name = dep[:name] || fail('You must provide a name for all dependencies')
|
246
|
+
version = dep[:version]
|
247
|
+
new(name, version, opts[:cwd], dep)
|
248
|
+
end
|
249
|
+
|
250
|
+
def to_s
|
251
|
+
@dep.to_s
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def pull_path(path)
|
257
|
+
abspath = File.absolute_path(path, @cwd)
|
258
|
+
fail "Dependency path doesn't exist: #{path}" unless File.exist?(abspath)
|
259
|
+
fail "Dependency path isn't a folder: #{path}" unless File.directory?(abspath)
|
260
|
+
@path = abspath
|
261
|
+
true
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
class SupermarketDependency
|
266
|
+
def initialize(url, requirement)
|
267
|
+
@url = url
|
268
|
+
@requirement = requirement
|
269
|
+
end
|
270
|
+
|
271
|
+
def self.load(dep)
|
272
|
+
return nil if dep.nil?
|
273
|
+
sname = dep[:supermarket]
|
274
|
+
return nil if sname.nil?
|
275
|
+
surl = dep[:supermarket_url] || 'default_url...'
|
276
|
+
requirement = dep[:version]
|
277
|
+
url = surl + '/' + sname
|
278
|
+
new(url, requirement)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
class Dependencies
|
283
|
+
attr_reader :list, :vendor_path
|
284
|
+
|
285
|
+
# initialize
|
286
|
+
#
|
287
|
+
# @param cwd [String] current working directory for relative path includes
|
288
|
+
# @param vendor_path [String] path which contains vendored dependencies
|
289
|
+
# @return [dependencies] this
|
290
|
+
def initialize(cwd, vendor_path)
|
291
|
+
@cwd = cwd
|
292
|
+
@vendor_path = vendor_path || File.join(Dir.home, '.inspec', 'cache')
|
293
|
+
@list = nil
|
294
|
+
end
|
295
|
+
|
296
|
+
# 1. Get dependencies, pull things to a local cache if necessary
|
297
|
+
# 2. Resolve dependencies
|
298
|
+
#
|
299
|
+
# @param dependencies [Gem::Dependency] list of dependencies
|
300
|
+
# @return [nil]
|
301
|
+
def vendor(dependencies)
|
302
|
+
return if dependencies.nil? || dependencies.empty?
|
303
|
+
@vendor_index ||= VendorIndex.new(@vendor_path)
|
304
|
+
@list = Resolver.resolve(dependencies, @vendor_index, @cwd)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|