-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
pip.rb
346 lines (284 loc) · 10.9 KB
/
pip.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# Puppet package provider for Python's `pip` package management frontend.
# <http://pip.pypa.io/>
require_relative '../../../puppet/util/package/version/pip'
require_relative '../../../puppet/util/package/version/range'
require_relative '../../../puppet/provider/package_targetable'
Puppet::Type.type(:package).provide :pip, :parent => ::Puppet::Provider::Package::Targetable do
desc "Python packages via `pip`.
This provider supports the `install_options` attribute, which allows command-line flags to be passed to pip.
These options should be specified as an array where each element is either a string or a hash."
has_feature :installable, :uninstallable, :upgradeable, :versionable, :version_ranges, :install_options, :targetable
PIP_VERSION = Puppet::Util::Package::Version::Pip
PIP_VERSION_RANGE = Puppet::Util::Package::Version::Range
# Override the specificity method to return 1 if pip is not set as default provider
def self.specificity
match = default_match
length = match ? match.length : 0
return 1 if length == 0
super
end
# Define the default provider package command name when the provider is targetable.
# Required by Puppet::Provider::Package::Targetable::resource_or_provider_command
def self.provider_command
# Ensure pip can upgrade pip, which usually puts pip into a new path /usr/local/bin/pip (compared to /usr/bin/pip)
self.cmd.map { |c| which(c) }.find { |c| c != nil }
end
def self.cmd
if Puppet::Util::Platform.windows?
["pip.exe"]
else
["pip", "pip-python", "pip2", "pip-2"]
end
end
def self.pip_version(command)
version = nil
execpipe [quote(command), '--version'] do |process|
process.collect do |line|
md = line.strip.match(/^pip (\d+\.\d+\.?\d*).*$/)
if md
version = md[1]
break
end
end
end
raise Puppet::Error, _("Cannot resolve pip version") unless version
version
end
# Return an array of structured information about every installed package
# that's managed by `pip` or an empty array if `pip` is not available.
def self.instances(target_command = nil)
if target_command
command = target_command
self.validate_command(command)
else
command = provider_command
end
packages = []
return packages unless command
command_options = ['freeze']
command_version = self.pip_version(command)
if compare_pip_versions(command_version, '8.1.0') >= 0
command_options << '--all'
end
execpipe [quote(command), command_options] do |process|
process.collect do |line|
pkg = parse(line)
next unless pkg
pkg[:command] = command
packages << new(pkg)
end
end
# Pip can also upgrade pip, but it's not listed in freeze so need to special case it
# Pip list would also show pip installed version, but "pip list" doesn't exist for older versions of pip (E.G v1.0)
# Not needed when "pip freeze --all" is available.
if compare_pip_versions(command_version, '8.1.0') == -1
packages << new({:ensure => command_version, :name => File.basename(command), :provider => name, :command => command})
end
packages
end
# Parse lines of output from `pip freeze`, which are structured as:
# _package_==_version_ or _package_===_version_
# or _package_ @ someURL@_version_
def self.parse(line)
if line.chomp =~ /^([^=]+)===?([^=]+)$/
{:ensure => $2, :name => $1, :provider => name}
elsif line.chomp =~ /^([^@]+) @ [^@]+@(.+)$/
{ :ensure => Regexp.last_match(2), :name => Regexp.last_match(1), :provider => name }
end
end
# Return structured information about a particular package or `nil`
# if the package is not installed or `pip` itself is not available.
def query
command = resource_or_provider_command
self.class.validate_command(command)
self.class.instances(command).each do |pkg|
return pkg.properties if @resource[:name].casecmp(pkg.name).zero?
end
return nil
end
# Return latest version available for current package
def latest
command = resource_or_provider_command
self.class.validate_command(command)
command_version = self.class.pip_version(command)
if self.class.compare_pip_versions(command_version, '1.5.4') == -1
available_versions_with_old_pip.last
else
available_versions_with_new_pip(command_version).last
end
end
def self.compare_pip_versions(x, y)
begin
Puppet::Util::Package::Version::Pip.compare(x, y)
rescue PIP_VERSION::ValidationFailure => ex
Puppet.debug("Cannot compare #{x} and #{y}. #{ex.message} Falling through default comparison mechanism.")
Puppet::Util::Package.versioncmp(x, y)
end
end
# Use pip CLI to look up versions from PyPI repositories,
# honoring local pip config such as custom repositories.
def available_versions
command = resource_or_provider_command
self.class.validate_command(command)
command_version = self.class.pip_version(command)
if self.class.compare_pip_versions(command_version, '1.5.4') == -1
available_versions_with_old_pip
else
available_versions_with_new_pip(command_version)
end
end
def available_versions_with_new_pip(command_version)
command = resource_or_provider_command
self.class.validate_command(command)
command_and_options = [self.class.quote(command), 'install', "#{@resource[:name]}==versionplease"]
extra_arg = list_extra_flags(command_version)
command_and_options << extra_arg if extra_arg
command_and_options << install_options if @resource[:install_options]
execpipe command_and_options do |process|
process.collect do |line|
# PIP OUTPUT: Could not find a version that satisfies the requirement example==versionplease (from versions: 1.2.3, 4.5.6)
if line =~ /from versions: (.+)\)/
versionList = $1.split(', ').sort do |x,y|
self.class.compare_pip_versions(x, y)
end
return versionList
end
end
end
[]
end
def available_versions_with_old_pip
command = resource_or_provider_command
self.class.validate_command(command)
Dir.mktmpdir("puppet_pip") do |dir|
command_and_options = [self.class.quote(command), 'install', "#{@resource[:name]}", '-d', "#{dir}", '-v']
command_and_options << install_options if @resource[:install_options]
execpipe command_and_options do |process|
process.collect do |line|
# PIP OUTPUT: Using version 0.10.1 (newest of versions: 1.2.3, 4.5.6)
if line =~ /Using version .+? \(newest of versions: (.+?)\)/
versionList = $1.split(', ').sort do |x,y|
self.class.compare_pip_versions(x, y)
end
return versionList
end
end
end
return []
end
end
# Finds the most suitable version available in a given range
def best_version(should_range)
included_available_versions = []
available_versions.each do |version|
version = PIP_VERSION.parse(version)
included_available_versions.push(version) if should_range.include?(version)
end
included_available_versions.sort!
return included_available_versions.last unless included_available_versions.empty?
Puppet.debug("No available version for package #{@resource[:name]} is included in range #{should_range}")
should_range
end
def get_install_command_options()
should = @resource[:ensure]
command_options = %w{install -q}
command_options += install_options if @resource[:install_options]
if @resource[:source]
if String === should
command_options << "#{@resource[:source]}@#{should}#egg=#{@resource[:name]}"
else
command_options << "#{@resource[:source]}#egg=#{@resource[:name]}"
end
return command_options
end
if should == :latest
command_options << "--upgrade" << @resource[:name]
return command_options
end
unless String === should
command_options << @resource[:name]
return command_options
end
begin
should_range = PIP_VERSION_RANGE.parse(should, PIP_VERSION)
rescue PIP_VERSION_RANGE::ValidationFailure, PIP_VERSION::ValidationFailure
Puppet.debug("Cannot parse #{should} as a pip version range, falling through.")
command_options << "#{@resource[:name]}==#{should}"
return command_options
end
if should_range.is_a?(PIP_VERSION_RANGE::Eq)
command_options << "#{@resource[:name]}==#{should}"
return command_options
end
should = best_version(should_range)
if should == should_range
# when no suitable version for the given range was found, let pip handle
if should.is_a?(PIP_VERSION_RANGE::MinMax)
command_options << "#{@resource[:name]} #{should.split.join(',')}"
else
command_options << "#{@resource[:name]} #{should}"
end
else
command_options << "#{@resource[:name]}==#{should}"
end
command_options
end
# Install a package. The ensure parameter may specify installed,
# latest, a version number, or, in conjunction with the source
# parameter, an SCM revision. In that case, the source parameter
# gives the fully-qualified URL to the repository.
def install
command = resource_or_provider_command
self.class.validate_command(command)
command_options = get_install_command_options
execute([command, command_options])
end
# Uninstall a package. Uninstall won't work reliably on Debian/Ubuntu unless this issue gets fixed.
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=562544
def uninstall
command = resource_or_provider_command
self.class.validate_command(command)
command_options = ["uninstall", "-y", "-q", @resource[:name]]
execute([command, command_options])
end
def update
install
end
def install_options
join_options(@resource[:install_options])
end
def insync?(is)
return false unless is && is != :absent
begin
should = @resource[:ensure]
should_range = PIP_VERSION_RANGE.parse(should, PIP_VERSION)
rescue PIP_VERSION_RANGE::ValidationFailure, PIP_VERSION::ValidationFailure
Puppet.debug("Cannot parse #{should} as a pip version range")
return false
end
begin
is_version = PIP_VERSION.parse(is)
rescue PIP_VERSION::ValidationFailure
Puppet.debug("Cannot parse #{is} as a pip version")
return false
end
should_range.include?(is_version)
end
# Quoting is required if the path to the pip command contains spaces.
# Required for execpipe() but not execute(), as execute() already does this.
def self.quote(path)
if path.include?(" ")
"\"#{path}\""
else
path
end
end
private
def list_extra_flags(command_version)
klass = self.class
if klass.compare_pip_versions(command_version, '20.2.4') == 1 &&
klass.compare_pip_versions(command_version, '21.1') == -1
'--use-deprecated=legacy-resolver'
end
end
end