tpkg 1.16.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +18 -0
- data/bin/cpan2tpkg +348 -0
- data/bin/gem2tpkg +445 -0
- data/bin/tpkg +560 -0
- data/lib/tpkg.rb +3966 -0
- data/lib/tpkg/deployer.rb +220 -0
- data/lib/tpkg/metadata.rb +436 -0
- data/lib/tpkg/thread_pool.rb +108 -0
- data/lib/tpkg/versiontype.rb +84 -0
- metadata +85 -0
data/lib/tpkg.rb
ADDED
@@ -0,0 +1,3966 @@
|
|
1
|
+
##############################################################################
|
2
|
+
# tpkg package management system library
|
3
|
+
# Copyright 2009, AT&T Interactive
|
4
|
+
# License: MIT (http://www.opensource.org/licenses/mit-license.php)
|
5
|
+
##############################################################################
|
6
|
+
|
7
|
+
STDOUT.sync = STDERR.sync = true # All outputs/prompts to the kernel ASAP
|
8
|
+
|
9
|
+
# When we build the tpkg packages we put this file in
|
10
|
+
# /usr/lib/ruby/site_ruby/1.8/ or similar and then the rest of the ruby
|
11
|
+
# files (versiontype.rb, deployer.rb, etc) into
|
12
|
+
# /usr/lib/ruby/site_ruby/1.8/tpkg/
|
13
|
+
# We need to tell Ruby to search that tpkg subdirectory.
|
14
|
+
# The alternative is to specify the subdirectory in the require
|
15
|
+
# (require 'tpkg/versiontype' for example), but tpkg is also the name
|
16
|
+
# of the executable script so we can't create a subdirectory here named
|
17
|
+
# tpkg. If we put the subdir in the require lines then users couldn't
|
18
|
+
# run tpkg directly from an svn working copy.
|
19
|
+
tpkglibdir = File.join(File.dirname(__FILE__), 'tpkg')
|
20
|
+
if File.directory?(tpkglibdir)
|
21
|
+
$:.unshift(tpkglibdir)
|
22
|
+
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
# Try loading facter w/o gems first so that we don't introduce a
|
26
|
+
# dependency on gems if it is not needed.
|
27
|
+
require 'facter' # Facter
|
28
|
+
rescue LoadError
|
29
|
+
require 'rubygems'
|
30
|
+
require 'facter'
|
31
|
+
end
|
32
|
+
require 'digest/sha2' # Digest::SHA256#hexdigest, etc.
|
33
|
+
require 'uri' # URI
|
34
|
+
require 'net/http' # Net::HTTP
|
35
|
+
require 'net/https' # Net::HTTP#use_ssl, etc.
|
36
|
+
require 'time' # Time#httpdate
|
37
|
+
require 'rexml/document' # REXML::Document
|
38
|
+
require 'fileutils' # FileUtils.cp, rm, etc.
|
39
|
+
require 'tempfile' # Tempfile
|
40
|
+
require 'find' # Find
|
41
|
+
require 'etc' # Etc.getpwnam, getgrnam
|
42
|
+
require 'openssl' # OpenSSL
|
43
|
+
require 'open3' # Open3
|
44
|
+
require 'versiontype' # Version
|
45
|
+
require 'deployer'
|
46
|
+
require 'set'
|
47
|
+
require 'metadata'
|
48
|
+
|
49
|
+
class Tpkg
|
50
|
+
|
51
|
+
VERSION = '1.16.2'
|
52
|
+
CONFIGDIR = '/etc'
|
53
|
+
|
54
|
+
POSTINSTALL_ERR = 2
|
55
|
+
POSTREMOVE_ERR = 3
|
56
|
+
INITSCRIPT_ERR = 4
|
57
|
+
|
58
|
+
attr_reader :installed_directory
|
59
|
+
|
60
|
+
#
|
61
|
+
# Class methods
|
62
|
+
#
|
63
|
+
|
64
|
+
@@debug = false
|
65
|
+
def self.set_debug(debug)
|
66
|
+
@@debug = debug
|
67
|
+
end
|
68
|
+
|
69
|
+
@@prompt = true
|
70
|
+
def self.set_prompt(prompt)
|
71
|
+
@@prompt = prompt
|
72
|
+
end
|
73
|
+
|
74
|
+
# Find GNU tar or bsdtar in ENV['PATH']
|
75
|
+
# Raises an exception if a suitable tar cannot be found
|
76
|
+
@@tar = nil
|
77
|
+
TARNAMES = ['tar', 'gtar', 'gnutar', 'bsdtar']
|
78
|
+
def self.find_tar
|
79
|
+
if !@@tar
|
80
|
+
catch :tar_found do
|
81
|
+
ENV['PATH'].split(':').each do |path|
|
82
|
+
TARNAMES.each do |tarname|
|
83
|
+
if File.executable?(File.join(path, tarname))
|
84
|
+
IO.popen("#{File.join(path, tarname)} --version 2>/dev/null") do |pipe|
|
85
|
+
pipe.each_line do |line|
|
86
|
+
if line.include?('GNU tar') || line.include?('bsdtar')
|
87
|
+
@@tar = File.join(path, tarname)
|
88
|
+
throw :tar_found
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
# Raise an exception if we didn't find a suitable tar
|
96
|
+
raise "Unable to find GNU tar or bsdtar in PATH"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
@@tar.dup
|
100
|
+
end
|
101
|
+
def self.clear_cached_tar
|
102
|
+
@@tar = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
# Encrypts the given file in-place (the plaintext file is replaced by the
|
106
|
+
# encrypted file). The resulting file is compatible with openssl's 'enc'
|
107
|
+
# utility.
|
108
|
+
# Algorithm from http://www.ruby-forum.com/topic/101936#225585
|
109
|
+
MAGIC = 'Salted__'
|
110
|
+
SALT_LEN = 8
|
111
|
+
@@passphrase = nil
|
112
|
+
def self.encrypt(pkgname, filename, passphrase, cipher='aes-256-cbc')
|
113
|
+
# passphrase can be a callback Proc, call it if that's the case
|
114
|
+
pass = nil
|
115
|
+
if @@passphrase
|
116
|
+
pass = @@passphrase
|
117
|
+
elsif passphrase.kind_of?(Proc)
|
118
|
+
pass = passphrase.call(pkgname)
|
119
|
+
@@passphrase = pass
|
120
|
+
else
|
121
|
+
pass = passphrase
|
122
|
+
end
|
123
|
+
|
124
|
+
salt = OpenSSL::Random::random_bytes(SALT_LEN)
|
125
|
+
c = OpenSSL::Cipher::Cipher.new(cipher)
|
126
|
+
c.encrypt
|
127
|
+
c.pkcs5_keyivgen(pass, salt, 1)
|
128
|
+
tmpfile = Tempfile.new(File.basename(filename), File.dirname(filename))
|
129
|
+
# Match permissions and ownership of plaintext file
|
130
|
+
st = File.stat(filename)
|
131
|
+
File.chmod(st.mode & 07777, tmpfile.path)
|
132
|
+
begin
|
133
|
+
File.chown(st.uid, st.gid, tmpfile.path)
|
134
|
+
rescue Errno::EPERM
|
135
|
+
raise if Process.euid == 0
|
136
|
+
end
|
137
|
+
tmpfile.write(MAGIC)
|
138
|
+
tmpfile.write(salt)
|
139
|
+
tmpfile.write(c.update(IO.read(filename)) + c.final)
|
140
|
+
tmpfile.close
|
141
|
+
File.rename(tmpfile.path, filename)
|
142
|
+
end
|
143
|
+
# Decrypt the given file in-place.
|
144
|
+
def self.decrypt(pkgname, filename, passphrase, cipher='aes-256-cbc')
|
145
|
+
# passphrase can be a callback Proc, call it if that's the case
|
146
|
+
pass = nil
|
147
|
+
if @@passphrase
|
148
|
+
pass = @@passphrase
|
149
|
+
elsif passphrase.kind_of?(Proc)
|
150
|
+
pass = passphrase.call(pkgname)
|
151
|
+
@@passphrase = pass
|
152
|
+
else
|
153
|
+
pass = passphrase
|
154
|
+
end
|
155
|
+
|
156
|
+
file = File.open(filename)
|
157
|
+
if (buf = file.read(MAGIC.length)) != MAGIC
|
158
|
+
raise "Unrecognized encrypted file #{filename}"
|
159
|
+
end
|
160
|
+
salt = file.read(SALT_LEN)
|
161
|
+
c = OpenSSL::Cipher::Cipher.new(cipher)
|
162
|
+
c.decrypt
|
163
|
+
c.pkcs5_keyivgen(pass, salt, 1)
|
164
|
+
tmpfile = Tempfile.new(File.basename(filename), File.dirname(filename))
|
165
|
+
# Match permissions and ownership of encrypted file
|
166
|
+
st = File.stat(filename)
|
167
|
+
File.chmod(st.mode & 07777, tmpfile.path)
|
168
|
+
begin
|
169
|
+
File.chown(st.uid, st.gid, tmpfile.path)
|
170
|
+
rescue Errno::EPERM
|
171
|
+
raise if Process.euid == 0
|
172
|
+
end
|
173
|
+
tmpfile.write(c.update(file.read) + c.final)
|
174
|
+
tmpfile.close
|
175
|
+
File.rename(tmpfile.path, filename)
|
176
|
+
end
|
177
|
+
def self.verify_precrypt_file(filename)
|
178
|
+
# This currently just verifies that the file seems to start with the
|
179
|
+
# right bits. Any further verification would require the passphrase
|
180
|
+
# and cipher so we could decrypt the file, but that would preclude
|
181
|
+
# folks from including precrypt files for which they don't have the
|
182
|
+
# passphrase in a package. In some environments it might be desirable
|
183
|
+
# for folks to be able to build the package even if they couldn't
|
184
|
+
# install it.
|
185
|
+
file = File.open(filename)
|
186
|
+
if (buf = file.read(MAGIC.length)) != MAGIC
|
187
|
+
raise "Unrecognized encrypted file #{filename}"
|
188
|
+
end
|
189
|
+
true
|
190
|
+
end
|
191
|
+
|
192
|
+
# Makes a package from a directory containing the files to put into the package
|
193
|
+
REQUIRED_FIELDS = ['name', 'version', 'maintainer']
|
194
|
+
def self.make_package(pkgsrcdir, passphrase=nil)
|
195
|
+
pkgfile = nil
|
196
|
+
|
197
|
+
# Make a working directory
|
198
|
+
workdir = nil
|
199
|
+
# dirname('.') returns '.', which screws things up. So in cases
|
200
|
+
# where the user passed us a directory that doesn't have enough
|
201
|
+
# parts that we can get the parent directory we use a working
|
202
|
+
# directory in the system's temp area. As an alternative we could
|
203
|
+
# use Pathname.realpath to convert whatever the user passed us into
|
204
|
+
# an absolute path.
|
205
|
+
if File.dirname(pkgsrcdir) == pkgsrcdir
|
206
|
+
workdir = tempdir('tpkg')
|
207
|
+
else
|
208
|
+
workdir = tempdir('tpkg', File.dirname(pkgsrcdir))
|
209
|
+
end
|
210
|
+
|
211
|
+
begin
|
212
|
+
# Make the 'tpkg' directory for storing the package contents
|
213
|
+
tpkgdir = File.join(workdir, 'tpkg')
|
214
|
+
Dir.mkdir(tpkgdir)
|
215
|
+
|
216
|
+
# A package really shouldn't be partially relocatable, warn the user if
|
217
|
+
# they're creating such a scourge.
|
218
|
+
if (File.exist?(File.join(pkgsrcdir, 'root')) && File.exist?(File.join(pkgsrcdir, 'reloc')))
|
219
|
+
warn 'Warning: Your source directory should contain either a "root" or "reloc" directory, but not both.'
|
220
|
+
end
|
221
|
+
|
222
|
+
# Copy the package contents into that directory
|
223
|
+
# I tried to use FileUtils.cp_r but it doesn't handle symlinks properly
|
224
|
+
# And on further reflection it makes sense to only have one chunk of
|
225
|
+
# code (tar) ever touch the user's files.
|
226
|
+
system("#{find_tar} -C #{pkgsrcdir} -cf - . | #{find_tar} -C #{tpkgdir} -xpf -") || raise("Package content copy failed")
|
227
|
+
|
228
|
+
if File.exists?(File.join(tpkgdir, 'tpkg.yml'))
|
229
|
+
metadata_text = File.read(File.join(tpkgdir, 'tpkg.yml'))
|
230
|
+
metadata = Metadata.new(metadata_text, 'yml')
|
231
|
+
elsif File.exists?(File.join(tpkgdir, 'tpkg.xml'))
|
232
|
+
metadata_text = File.read(File.join(tpkgdir, 'tpkg.xml'))
|
233
|
+
metadata = Metadata.new(metadata_text, 'xml')
|
234
|
+
else
|
235
|
+
raise 'Your source directory does not contain the metadata configuration file.'
|
236
|
+
end
|
237
|
+
|
238
|
+
metadata.verify_required_fields
|
239
|
+
|
240
|
+
# file_metadata.yml hold information for files that are installed
|
241
|
+
# by the package. For example, checksum, path, relocatable or not, etc.
|
242
|
+
File.open(File.join(tpkgdir, "file_metadata.bin"), "w") do |file|
|
243
|
+
filemetadata = get_filemetadata_from_directory(tpkgdir)
|
244
|
+
Marshal::dump(filemetadata.hash, file)
|
245
|
+
# YAML::dump(filemetadata.hash, file)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Check all the files are there as specified in the metadata config file
|
249
|
+
metadata[:files][:files].each do |tpkgfile|
|
250
|
+
tpkg_path = tpkgfile[:path]
|
251
|
+
working_path = nil
|
252
|
+
if tpkg_path[0,1] == File::SEPARATOR
|
253
|
+
working_path = File.join(tpkgdir, 'root', tpkg_path)
|
254
|
+
else
|
255
|
+
working_path = File.join(tpkgdir, 'reloc', tpkg_path)
|
256
|
+
end
|
257
|
+
# Raise an exception if any files listed in tpkg.yml can't be found
|
258
|
+
if !File.exist?(working_path) && !File.symlink?(working_path)
|
259
|
+
raise "File #{tpkg_path} referenced in tpkg.yml but not found"
|
260
|
+
end
|
261
|
+
|
262
|
+
# Encrypt any files marked for encryption
|
263
|
+
if tpkgfile[:encrypt]
|
264
|
+
if tpkgfile[:encrypt] == 'precrypt'
|
265
|
+
verify_precrypt_file(working_path)
|
266
|
+
else
|
267
|
+
if passphrase.nil?
|
268
|
+
raise "Package requires encryption but supplied passphrase is nil"
|
269
|
+
end
|
270
|
+
encrypt(metadata[:name], working_path, passphrase)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end unless metadata[:files].nil? or metadata[:files][:files].nil?
|
274
|
+
|
275
|
+
package_filename = metadata.generate_package_filename
|
276
|
+
package_directory = File.join(workdir, package_filename)
|
277
|
+
Dir.mkdir(package_directory)
|
278
|
+
pkgfile = File.join(File.dirname(pkgsrcdir), package_filename + '.tpkg')
|
279
|
+
if File.exist?(pkgfile) || File.symlink?(pkgfile)
|
280
|
+
if @@prompt
|
281
|
+
print "Package file #{pkgfile} already exists, overwrite? [y/N]"
|
282
|
+
response = $stdin.gets
|
283
|
+
if response !~ /^y/i
|
284
|
+
return
|
285
|
+
end
|
286
|
+
end
|
287
|
+
File.delete(pkgfile)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Tar up the tpkg directory
|
291
|
+
tpkgfile = File.join(package_directory, 'tpkg.tar')
|
292
|
+
system("#{find_tar} -C #{workdir} -cf #{tpkgfile} tpkg") || raise("tpkg.tar creation failed")
|
293
|
+
|
294
|
+
# Checksum the tarball
|
295
|
+
# Older ruby version doesn't support this
|
296
|
+
# digest = Digest::SHA256.file(tpkgfile).hexdigest
|
297
|
+
digest = Digest::SHA256.hexdigest(File.read(tpkgfile))
|
298
|
+
|
299
|
+
# Create checksum.xml
|
300
|
+
File.open(File.join(package_directory, 'checksum.xml'), 'w') do |csx|
|
301
|
+
csx.puts('<tpkg_checksums>')
|
302
|
+
csx.puts(' <checksum>')
|
303
|
+
csx.puts(' <algorithm>SHA256</algorithm>')
|
304
|
+
csx.puts(" <digest>#{digest}</digest>")
|
305
|
+
csx.puts(' </checksum>')
|
306
|
+
csx.puts('</tpkg_checksums>')
|
307
|
+
end
|
308
|
+
|
309
|
+
# Tar up checksum.xml and the main tarball
|
310
|
+
system("#{find_tar} -C #{workdir} -cf #{pkgfile} #{package_filename}") || raise("Final package creation failed")
|
311
|
+
ensure
|
312
|
+
# Remove our working directory
|
313
|
+
FileUtils.rm_rf(workdir)
|
314
|
+
end
|
315
|
+
|
316
|
+
# Return the filename of the package
|
317
|
+
pkgfile
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.package_toplevel_directory(package_file)
|
321
|
+
# This assumes the first entry in the tarball is the top level directory.
|
322
|
+
# I think that is a safe assumption.
|
323
|
+
toplevel = nil
|
324
|
+
# FIXME: This is so lame, to read the whole package to get the
|
325
|
+
# first filename. Blech.
|
326
|
+
IO.popen("#{find_tar} -tf #{package_file}") do |pipe|
|
327
|
+
toplevel = pipe.gets.chomp
|
328
|
+
# Avoid SIGPIPE, if we don't sink the rest of the output from tar
|
329
|
+
# then tar ends up getting SIGPIPE when it tries to write to the
|
330
|
+
# closed pipe and exits with error, which causes us to throw an
|
331
|
+
# exception down below here when we check the exit status.
|
332
|
+
pipe.read
|
333
|
+
end
|
334
|
+
if !$?.success?
|
335
|
+
raise "Error reading top level directory from #{package_file}"
|
336
|
+
end
|
337
|
+
# Strip off the trailing slash
|
338
|
+
toplevel.sub!(Regexp.new("#{File::SEPARATOR}$"), '')
|
339
|
+
if toplevel.include?(File::SEPARATOR)
|
340
|
+
raise "Package directory structure of #{package_file} unexpected, top level is more than one directory deep"
|
341
|
+
end
|
342
|
+
toplevel
|
343
|
+
end
|
344
|
+
|
345
|
+
def self.get_filemetadata_from_directory(tpkgdir)
|
346
|
+
filemetadata = {}
|
347
|
+
root_dir = File.join(tpkgdir, "root")
|
348
|
+
reloc_dir = File.join(tpkgdir, "reloc")
|
349
|
+
files = []
|
350
|
+
|
351
|
+
Find.find(root_dir, reloc_dir) do |f|
|
352
|
+
next if !File.exist?(f)
|
353
|
+
relocatable = false
|
354
|
+
|
355
|
+
# check if it's from root dir or reloc dir
|
356
|
+
if f =~ /^#{root_dir}/
|
357
|
+
short_fn = f[root_dir.length ..-1]
|
358
|
+
else
|
359
|
+
short_fn = f[reloc_dir.length + 1..-1]
|
360
|
+
relocatable = true
|
361
|
+
end
|
362
|
+
|
363
|
+
next if short_fn.nil? or short_fn.empty?
|
364
|
+
|
365
|
+
file = {}
|
366
|
+
file[:path] = short_fn
|
367
|
+
file[:relocatable] = relocatable
|
368
|
+
|
369
|
+
# only do checksum for file
|
370
|
+
if File.file?(f)
|
371
|
+
digest = Digest::SHA256.hexdigest(File.read(f))
|
372
|
+
file[:checksum] = {:algorithm => "SHA256", :digests => [{:value => digest}]}
|
373
|
+
end
|
374
|
+
files << file
|
375
|
+
end
|
376
|
+
filemetadata['files'] = files
|
377
|
+
#return FileMetadata.new(YAML::dump(filemetadata),'yml')
|
378
|
+
return FileMetadata.new(Marshal::dump(filemetadata),'bin')
|
379
|
+
end
|
380
|
+
|
381
|
+
def self.get_xml_filemetadata_from_directory(tpkgdir)
|
382
|
+
filemetadata_xml = REXML::Document.new
|
383
|
+
filemetadata_xml << REXML::Element.new('files')
|
384
|
+
|
385
|
+
# create file_metadata.xml that stores list of files and their checksum
|
386
|
+
# will be used later on to check whether installed files have been changed
|
387
|
+
root_dir = File.join(tpkgdir, "root")
|
388
|
+
reloc_dir = File.join(tpkgdir, "reloc")
|
389
|
+
Find.find(root_dir, reloc_dir) do |f|
|
390
|
+
next if !File.exist?(f)
|
391
|
+
relocatable = "false"
|
392
|
+
|
393
|
+
# check if it's from root dir or reloc dir
|
394
|
+
if f =~ /^#{root_dir}/
|
395
|
+
short_fn = f[root_dir.length ..-1]
|
396
|
+
else
|
397
|
+
short_fn = f[reloc_dir.length + 1..-1]
|
398
|
+
relocatable = "true"
|
399
|
+
end
|
400
|
+
|
401
|
+
next if short_fn.nil? or short_fn.empty?
|
402
|
+
|
403
|
+
file_ele = filemetadata_xml.root.add_element("file", {"relocatable" => relocatable})
|
404
|
+
path_ele = file_ele.add_element("path")
|
405
|
+
path_ele.add_text(short_fn)
|
406
|
+
|
407
|
+
# only do checksum for file
|
408
|
+
if File.file?(f)
|
409
|
+
# this doesn't work for older ruby version
|
410
|
+
#digest = Digest::SHA256.file(f).hexdigest
|
411
|
+
digest = Digest::SHA256.hexdigest(File.read(f))
|
412
|
+
chksum_ele = file_ele.add_element("checksum")
|
413
|
+
alg_ele = chksum_ele.add_element("algorithm")
|
414
|
+
alg_ele.add_text("SHA256")
|
415
|
+
digest_ele = chksum_ele.add_element("digest")
|
416
|
+
digest_ele.add_text(digest)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
return filemetadata_xml
|
420
|
+
end
|
421
|
+
|
422
|
+
def self.verify_package_checksum(package_file)
|
423
|
+
topleveldir = package_toplevel_directory(package_file)
|
424
|
+
# Extract checksum.xml from the package
|
425
|
+
checksum_xml = nil
|
426
|
+
IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'checksum.xml')}") do |pipe|
|
427
|
+
checksum_xml = REXML::Document.new(pipe.read)
|
428
|
+
end
|
429
|
+
if !$?.success?
|
430
|
+
raise "Error extracting checksum.xml from #{package_file}"
|
431
|
+
end
|
432
|
+
|
433
|
+
# Verify checksum.xml
|
434
|
+
checksum_xml.elements.each('/tpkg_checksums/checksum') do |checksum|
|
435
|
+
digest = nil
|
436
|
+
algorithm = checksum.elements['algorithm'].text
|
437
|
+
digest_from_package = checksum.elements['digest'].text
|
438
|
+
case algorithm
|
439
|
+
when 'SHA224'
|
440
|
+
digest = Digest::SHA224.new
|
441
|
+
when 'SHA256'
|
442
|
+
digest = Digest::SHA256.new
|
443
|
+
when 'SHA384'
|
444
|
+
digest = Digest::SHA384.new
|
445
|
+
when 'SHA512'
|
446
|
+
digest = Digest::SHA512.new
|
447
|
+
else
|
448
|
+
raise("Unrecognized checksum algorithm #{checksum.elements['algorithm']}")
|
449
|
+
end
|
450
|
+
# Extract tpkg.tar from the package and digest it
|
451
|
+
IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')}") do |pipe|
|
452
|
+
# Package files can be quite large, so we digest the package in
|
453
|
+
# chunks. A survey of the Internet turns up someone who tested
|
454
|
+
# various chunk sizes on various platforms and found 4k to be
|
455
|
+
# consistently the best. I'm too lazy to do my own testing.
|
456
|
+
# http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/721d304fc8a5cc71
|
457
|
+
while buf = pipe.read(4096)
|
458
|
+
digest << buf
|
459
|
+
end
|
460
|
+
end
|
461
|
+
if !$?.success?
|
462
|
+
raise "Error extracting tpkg.tar from #{package_file}"
|
463
|
+
end
|
464
|
+
if digest != digest_from_package
|
465
|
+
raise "Checksum mismatch for #{algorithm}, #{digest} != #{digest_from_package}"
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
# Extracts and returns the metadata from a package file
|
471
|
+
def self.metadata_from_package(package_file)
|
472
|
+
topleveldir = package_toplevel_directory(package_file)
|
473
|
+
# Verify checksum
|
474
|
+
verify_package_checksum(package_file)
|
475
|
+
# Extract and parse tpkg.xml
|
476
|
+
metadata = nil
|
477
|
+
['yml','xml'].each do |format|
|
478
|
+
file = File.join('tpkg', "tpkg.#{format}")
|
479
|
+
|
480
|
+
# use popen3 instead of popen because popen display stderr when there's an error such as
|
481
|
+
# tpkg.yml not being there, which is something we want to ignore since old tpkg doesn't
|
482
|
+
# have tpkg.yml file
|
483
|
+
stdin, stdout, stderr = Open3.popen3("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{find_tar} -xf - -O #{file}")
|
484
|
+
filecontent = stdout.read
|
485
|
+
if filecontent.nil? or filecontent.empty?
|
486
|
+
next
|
487
|
+
else
|
488
|
+
metadata = Metadata.new(filecontent, format)
|
489
|
+
break
|
490
|
+
end
|
491
|
+
end
|
492
|
+
unless metadata
|
493
|
+
raise "Failed to extract metadata from #{package_file}"
|
494
|
+
end
|
495
|
+
|
496
|
+
# Insert an attribute on the root element with the package filename
|
497
|
+
metadata[:filename] = File.basename(package_file)
|
498
|
+
return metadata
|
499
|
+
end
|
500
|
+
|
501
|
+
# TODO: To be deprecated
|
502
|
+
# Extracts and returns the metadata from a package file
|
503
|
+
def self.xml_metadata_from_package(package_file)
|
504
|
+
topleveldir = package_toplevel_directory(package_file)
|
505
|
+
# Verify checksum
|
506
|
+
verify_package_checksum(package_file)
|
507
|
+
# Extract and parse tpkg.xml
|
508
|
+
tpkg_xml = nil
|
509
|
+
IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{find_tar} -xf - -O #{File.join('tpkg', 'tpkg.xml')}") do |pipe|
|
510
|
+
tpkg_xml = REXML::Document.new(pipe.read)
|
511
|
+
end
|
512
|
+
if !$?.success?
|
513
|
+
raise "Extracting tpkg.xml from #{package_file} failed"
|
514
|
+
end
|
515
|
+
|
516
|
+
# Insert an attribute on the root element with the package filename
|
517
|
+
tpkg_xml.root.attributes['filename'] = File.basename(package_file)
|
518
|
+
|
519
|
+
# Return
|
520
|
+
return tpkg_xml
|
521
|
+
end
|
522
|
+
|
523
|
+
# TODO: To be deprecated
|
524
|
+
# Extracts and returns the metadata from a directory of package files
|
525
|
+
def self.xml_metadata_from_directory(directory)
|
526
|
+
metadata = []
|
527
|
+
# if metadata.xml already exists, then go ahead and
|
528
|
+
# parse it
|
529
|
+
existing_metadata_file = File.join(directory, 'metadata.xml')
|
530
|
+
existing_metadata = {}
|
531
|
+
if File.exists?(existing_metadata_file)
|
532
|
+
tpkg_metadata_xml = REXML::Document.new(File.open(existing_metadata_file))
|
533
|
+
|
534
|
+
tpkg_metadata_xml.root.elements.each do | metadata_xml |
|
535
|
+
existing_metadata[metadata_xml.attributes['filename']] = metadata_xml
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Populate the metadata array with metadata for all of the packages
|
540
|
+
# in the given directory. Reuse existing metadata if possible.
|
541
|
+
Dir.glob(File.join(directory, '*.tpkg')) do |pkg|
|
542
|
+
if existing_metadata[File.basename(pkg)]
|
543
|
+
metadata << existing_metadata[File.basename(pkg)]
|
544
|
+
else
|
545
|
+
xml = xml_metadata_from_package(pkg)
|
546
|
+
metadata << xml.root
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
return metadata
|
551
|
+
end
|
552
|
+
|
553
|
+
# Extracts and returns the metadata from a directory of package files
|
554
|
+
def self.metadata_from_directory(directory)
|
555
|
+
metadata = []
|
556
|
+
|
557
|
+
# if metadata.xml already exists, then go ahead and
|
558
|
+
# parse it
|
559
|
+
existing_metadata_file = File.join(directory, 'metadata.yml')
|
560
|
+
existing_metadata = {}
|
561
|
+
|
562
|
+
if File.exists?(existing_metadata_file)
|
563
|
+
metadata_contents = File.read(File.join(directory, 'metadata.yml'))
|
564
|
+
Metadata::get_pkgs_metadata_from_yml_doc(metadata_contents, existing_metadata)
|
565
|
+
end
|
566
|
+
|
567
|
+
# Populate the metadata array with metadata for all of the packages
|
568
|
+
# in the given directory. Reuse existing metadata if possible.
|
569
|
+
Dir.glob(File.join(directory, '*.tpkg')) do |pkg|
|
570
|
+
if existing_metadata[File.basename(pkg)]
|
571
|
+
metadata << existing_metadata[File.basename(pkg)]
|
572
|
+
else
|
573
|
+
metadata_yml = metadata_from_package(pkg)
|
574
|
+
metadata << metadata_yml
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
return metadata
|
579
|
+
end
|
580
|
+
|
581
|
+
# Extracts the metadata from a directory of package files and saves it
|
582
|
+
# to metadata.xml in that directory
|
583
|
+
def self.extract_metadata(directory, dest=nil)
|
584
|
+
dest = directory if dest.nil?
|
585
|
+
backward_compatible = true
|
586
|
+
|
587
|
+
# If we still want to support metadata.xml
|
588
|
+
if backward_compatible
|
589
|
+
metadata_xml = xml_metadata_from_directory(directory)
|
590
|
+
# Combine all of the individual metadata files into one XML document
|
591
|
+
metadata = REXML::Document.new
|
592
|
+
metadata << REXML::Element.new('tpkg_metadata')
|
593
|
+
metadata_xml.each do |md|
|
594
|
+
metadata.root << md
|
595
|
+
end
|
596
|
+
# And write that out to metadata.xml
|
597
|
+
metadata_tmpfile = Tempfile.new('metadata.xml', dest)
|
598
|
+
metadata.write(metadata_tmpfile)
|
599
|
+
metadata_tmpfile.close
|
600
|
+
File.chmod(0644, metadata_tmpfile.path)
|
601
|
+
File.rename(metadata_tmpfile.path, File.join(dest, 'metadata.xml'))
|
602
|
+
end
|
603
|
+
|
604
|
+
metadata = metadata_from_directory(directory)
|
605
|
+
# And write that out to metadata.yml
|
606
|
+
metadata_tmpfile = Tempfile.new('metadata.yml', dest)
|
607
|
+
metadata.each do | metadata |
|
608
|
+
YAML::dump(metadata.hash, metadata_tmpfile)
|
609
|
+
end
|
610
|
+
metadata_tmpfile.close
|
611
|
+
File.chmod(0644, metadata_tmpfile.path)
|
612
|
+
File.rename(metadata_tmpfile.path, File.join(dest, 'metadata.yml'))
|
613
|
+
end
|
614
|
+
|
615
|
+
# Haven't found a Ruby method for creating temporary directories,
|
616
|
+
# so create a temporary file and replace it with a directory.
|
617
|
+
def self.tempdir(basename, tmpdir=Dir::tmpdir)
|
618
|
+
tmpfile = Tempfile.new(basename, tmpdir)
|
619
|
+
tmpdir = tmpfile.path
|
620
|
+
tmpfile.close!
|
621
|
+
Dir.mkdir(tmpdir)
|
622
|
+
tmpdir
|
623
|
+
end
|
624
|
+
|
625
|
+
@@arch = nil
|
626
|
+
def self.get_arch
|
627
|
+
if !@@arch
|
628
|
+
Facter.loadfacts
|
629
|
+
@@arch = Facter['hardwaremodel'].value
|
630
|
+
end
|
631
|
+
@@arch.dup
|
632
|
+
end
|
633
|
+
|
634
|
+
# Returns a string representing the OS of this box of the form:
|
635
|
+
# "OSname-OSmajorversion". The OS name is currently whatever facter
|
636
|
+
# returns for the 'operatingsystem' fact. The major version is a bit
|
637
|
+
# messier, as we try on a per-OS basis to come up with something that
|
638
|
+
# represents the major version number of the OS, where binaries are
|
639
|
+
# expected to be compatible across all versions of the OS with that
|
640
|
+
# same major version number. Examples include RedHat-5, CentOS-5,
|
641
|
+
# FreeBSD-7, Darwin-10.5, and Solaris-5.10
|
642
|
+
@@os = nil
|
643
|
+
def self.get_os
|
644
|
+
if !@@os
|
645
|
+
# Tell facter to load everything, otherwise it tries to dynamically
|
646
|
+
# load the individual fact libraries using a very broken mechanism
|
647
|
+
Facter.loadfacts
|
648
|
+
|
649
|
+
operatingsystem = Facter['operatingsystem'].value
|
650
|
+
osver = nil
|
651
|
+
if Facter['lsbmajdistrelease'] &&
|
652
|
+
Facter['lsbmajdistrelease'].value &&
|
653
|
+
!Facter['lsbmajdistrelease'].value.empty?
|
654
|
+
osver = Facter['lsbmajdistrelease'].value
|
655
|
+
elsif Facter['kernel'] &&
|
656
|
+
Facter['kernel'].value == 'Darwin' &&
|
657
|
+
Facter['macosx_productversion'] &&
|
658
|
+
Facter['macosx_productversion'].value &&
|
659
|
+
!Facter['macosx_productversion'].value.empty?
|
660
|
+
macver = Facter['macosx_productversion'].value
|
661
|
+
# Extract 10.5 from 10.5.6, for example
|
662
|
+
osver = macver.split('.')[0,2].join('.')
|
663
|
+
elsif Facter['operatingsystem'] &&
|
664
|
+
Facter['operatingsystem'].value == 'FreeBSD'
|
665
|
+
# Extract 7 from 7.1-RELEASE, for example
|
666
|
+
fbver = Facter['operatingsystemrelease'].value
|
667
|
+
osver = fbver.split('.').first
|
668
|
+
elsif Facter['operatingsystemrelease'] &&
|
669
|
+
Facter['operatingsystemrelease'].value &&
|
670
|
+
!Facter['operatingsystemrelease'].value.empty?
|
671
|
+
osver = Facter['operatingsystemrelease'].value
|
672
|
+
else
|
673
|
+
raise "Unable to determine proper OS value on this platform"
|
674
|
+
end
|
675
|
+
@@os = "#{operatingsystem}-#{osver}"
|
676
|
+
end
|
677
|
+
@@os.dup
|
678
|
+
end
|
679
|
+
|
680
|
+
# Given an array of pkgs. Determine if any of those package
|
681
|
+
# satisfy the requirement specified by req
|
682
|
+
def self.packages_meet_requirement?(pkgs, req)
|
683
|
+
pkgs.each do | pkg |
|
684
|
+
return true if Tpkg::package_meets_requirement?(pkg, req)
|
685
|
+
end
|
686
|
+
return false
|
687
|
+
end
|
688
|
+
|
689
|
+
# pkg is a standard Hash format used in the library to represent an
|
690
|
+
# available package
|
691
|
+
# req is a standard Hash format used in the library to represent package
|
692
|
+
# requirements
|
693
|
+
def self.package_meets_requirement?(pkg, req)
|
694
|
+
result = true
|
695
|
+
puts "pkg_meets_req checking #{pkg.inspect} against #{req.inspect}" if @@debug
|
696
|
+
metadata = pkg[:metadata]
|
697
|
+
if req[:type] == :native && pkg[:source] != :native_installed && pkg[:source] != :native_available
|
698
|
+
# A req for a native package must be satisfied by a native package
|
699
|
+
puts "Package fails native requirement" if @@debug
|
700
|
+
result = false
|
701
|
+
elsif (!req[:type] || req[:type] == :tpkg) &&
|
702
|
+
(pkg[:source] == :native_installed || pkg[:source] == :native_available)
|
703
|
+
# Likewise a req for a tpkg must be satisfied by a tpkg
|
704
|
+
puts "Package fails non-native requirement" if @@debug
|
705
|
+
result = false
|
706
|
+
elsif metadata[:name] == req[:name]
|
707
|
+
same_min_ver_req = false
|
708
|
+
same_max_ver_req = false
|
709
|
+
if req[:allowed_versions]
|
710
|
+
version = metadata[:version]
|
711
|
+
version = "#{version}-#{metadata[:package_version]}" if metadata[:package_version]
|
712
|
+
if !File.fnmatch(req[:allowed_versions], version)
|
713
|
+
puts "Package fails version requirement.)" if @@debug
|
714
|
+
result = false
|
715
|
+
end
|
716
|
+
end
|
717
|
+
if req[:minimum_version]
|
718
|
+
pkgver = Version.new(metadata[:version])
|
719
|
+
reqver = Version.new(req[:minimum_version])
|
720
|
+
if pkgver < reqver
|
721
|
+
puts "Package fails minimum_version (#{pkgver} < #{reqver})" if @@debug
|
722
|
+
result = false
|
723
|
+
elsif pkgver == reqver
|
724
|
+
same_min_ver_req = true
|
725
|
+
end
|
726
|
+
end
|
727
|
+
if req[:maximum_version]
|
728
|
+
pkgver = Version.new(metadata[:version])
|
729
|
+
reqver = Version.new(req[:maximum_version])
|
730
|
+
if pkgver > reqver
|
731
|
+
puts "Package fails maximum_version (#{pkgver} > #{reqver})" if @@debug
|
732
|
+
result = false
|
733
|
+
elsif pkgver == reqver
|
734
|
+
same_max_ver_req = true
|
735
|
+
end
|
736
|
+
end
|
737
|
+
if same_min_ver_req && req[:minimum_package_version]
|
738
|
+
pkgver = Version.new(metadata[:package_version])
|
739
|
+
reqver = Version.new(req[:minimum_package_version])
|
740
|
+
if pkgver < reqver
|
741
|
+
puts "Package fails minimum_package_version (#{pkgver} < #{reqver})" if @@debug
|
742
|
+
result = false
|
743
|
+
end
|
744
|
+
end
|
745
|
+
if same_max_ver_req && req[:maximum_package_version]
|
746
|
+
pkgver = Version.new(metadata[:package_version])
|
747
|
+
reqver = Version.new(req[:maximum_package_version])
|
748
|
+
if pkgver > reqver
|
749
|
+
puts "Package fails maximum_package_version (#{pkgver} > #{reqver})" if @@debug
|
750
|
+
result = false
|
751
|
+
end
|
752
|
+
end
|
753
|
+
# The empty? check ensures that a package with no operatingsystem
|
754
|
+
# field matches all clients.
|
755
|
+
if metadata[:operatingsystem] &&
|
756
|
+
!metadata[:operatingsystem].empty? &&
|
757
|
+
!metadata[:operatingsystem].include?(get_os) &&
|
758
|
+
!metadata[:operatingsystem].any?{|os| get_os =~ /#{os}/}
|
759
|
+
puts "Package fails operatingsystem" if @@debug
|
760
|
+
result = false
|
761
|
+
end
|
762
|
+
# Same deal with empty? here
|
763
|
+
if metadata[:architecture] &&
|
764
|
+
!metadata[:architecture].empty? &&
|
765
|
+
!metadata[:architecture].include?(get_arch) &&
|
766
|
+
!metadata[:architecture].any?{|arch| get_arch =~ /#{arch}/}
|
767
|
+
puts "Package fails architecture" if @@debug
|
768
|
+
result = false
|
769
|
+
end
|
770
|
+
else
|
771
|
+
puts "Package fails name" if @@debug
|
772
|
+
result = false
|
773
|
+
end
|
774
|
+
result
|
775
|
+
end
|
776
|
+
|
777
|
+
# Define a block for sorting packages in order of desirability
|
778
|
+
# Suitable for passing to Array#sort as array.sort(&SORT_PACKAGES)
|
779
|
+
SORT_PACKAGES = lambda do |a,b|
|
780
|
+
#
|
781
|
+
# We first prepare all of the values we wish to compare
|
782
|
+
#
|
783
|
+
|
784
|
+
# Name
|
785
|
+
aname = a[:metadata][:name]
|
786
|
+
bname = b[:metadata][:name]
|
787
|
+
# Currently installed
|
788
|
+
# Conflicted about whether this belongs here or not, not sure if all
|
789
|
+
# potential users of this sorting system would want to prefer currently
|
790
|
+
# installed packages.
|
791
|
+
acurrentinstall = 0
|
792
|
+
if (a[:source] == :currently_installed || a[:source] == :native_installed) && a[:prefer] == true
|
793
|
+
acurrentinstall = 1
|
794
|
+
end
|
795
|
+
bcurrentinstall = 0
|
796
|
+
if (b[:source] == :currently_installed || b[:source] == :native_installed) && b[:prefer] == true
|
797
|
+
bcurrentinstall = 1
|
798
|
+
end
|
799
|
+
# Version
|
800
|
+
aversion = Version.new(a[:metadata][:version])
|
801
|
+
bversion = Version.new(b[:metadata][:version])
|
802
|
+
# Package version
|
803
|
+
apkgver = Version.new(0)
|
804
|
+
if a[:metadata][:package_version]
|
805
|
+
apkgver = Version.new(a[:metadata][:package_version])
|
806
|
+
end
|
807
|
+
bpkgver = Version.new(0)
|
808
|
+
if b[:metadata][:package_version]
|
809
|
+
bpkgver = Version.new(b[:metadata][:package_version])
|
810
|
+
end
|
811
|
+
# OS
|
812
|
+
# Fewer OSs is better, but zero is least desirable because zero means
|
813
|
+
# the package works on all OSs (i.e. it is the most generic package).
|
814
|
+
# We prefer packages tuned to a particular set of OSs over packages
|
815
|
+
# that work everywhere on the assumption that the package that works
|
816
|
+
# on only a few platforms was tuned more specifically for those
|
817
|
+
# platforms. We remap 0 to a big number so that the sorting works
|
818
|
+
# properly.
|
819
|
+
aoslength = 0
|
820
|
+
aoslength = a[:metadata][:operatingsystem].length if a[:metadata][:operatingsystem]
|
821
|
+
if aoslength == 0
|
822
|
+
# See comments above
|
823
|
+
aoslength = 1000
|
824
|
+
end
|
825
|
+
boslength = 0
|
826
|
+
boslength = b[:metadata][:operatingsystem].length if b[:metadata][:operatingsystem]
|
827
|
+
if boslength == 0
|
828
|
+
boslength = 1000
|
829
|
+
end
|
830
|
+
# Architecture
|
831
|
+
# Same deal here, fewer architectures is better but zero is least desirable
|
832
|
+
aarchlength = 0
|
833
|
+
aarchlength = a[:metadata][:architecture].length if a[:metadata][:architecture]
|
834
|
+
if aarchlength == 0
|
835
|
+
aarchlength = 1000
|
836
|
+
end
|
837
|
+
barchlength = 0
|
838
|
+
barchlength = b[:metadata][:architecture].length if b[:metadata][:architecture]
|
839
|
+
if barchlength == 0
|
840
|
+
barchlength = 1000
|
841
|
+
end
|
842
|
+
# Prefer a currently installed package over an otherwise identical
|
843
|
+
# not installed package even if :prefer==false as a last deciding
|
844
|
+
# factor.
|
845
|
+
acurrentinstallnoprefer = 0
|
846
|
+
if a[:source] == :currently_installed || a[:source] == :native_installed
|
847
|
+
acurrentinstallnoprefer = 1
|
848
|
+
end
|
849
|
+
bcurrentinstallnoprefer = 0
|
850
|
+
if b[:source] == :currently_installed || b[:source] == :native_installed
|
851
|
+
bcurrentinstallnoprefer = 1
|
852
|
+
end
|
853
|
+
|
854
|
+
#
|
855
|
+
# Then compare
|
856
|
+
#
|
857
|
+
|
858
|
+
# The mixture of a's and b's in these two arrays may seem odd at first,
|
859
|
+
# but for some fields bigger is better (versions) but for other fields
|
860
|
+
# smaller is better.
|
861
|
+
[aname, bcurrentinstall, bversion, bpkgver, aoslength,
|
862
|
+
aarchlength, bcurrentinstallnoprefer] <=>
|
863
|
+
[bname, acurrentinstall, aversion, apkgver, boslength,
|
864
|
+
barchlength, acurrentinstallnoprefer]
|
865
|
+
end
|
866
|
+
|
867
|
+
def self.files_in_package(package_file)
|
868
|
+
files = {}
|
869
|
+
files[:root] = []
|
870
|
+
files[:reloc] = []
|
871
|
+
topleveldir = package_toplevel_directory(package_file)
|
872
|
+
IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{find_tar} -tf -") do |pipe|
|
873
|
+
pipe.each do |file|
|
874
|
+
file.chomp!
|
875
|
+
if file =~ Regexp.new(File.join('tpkg', 'root'))
|
876
|
+
files[:root] << file.sub(Regexp.new(File.join('tpkg', 'root')), '')
|
877
|
+
elsif file =~ Regexp.new(File.join('tpkg', 'reloc', '.'))
|
878
|
+
files[:reloc] << file.sub(Regexp.new(File.join('tpkg', 'reloc', '')), '')
|
879
|
+
end
|
880
|
+
end
|
881
|
+
end
|
882
|
+
if !$?.success?
|
883
|
+
raise "Extracting file list from #{package_file} failed"
|
884
|
+
end
|
885
|
+
files
|
886
|
+
end
|
887
|
+
|
888
|
+
def self.lookup_uid(user)
|
889
|
+
uid = nil
|
890
|
+
if user =~ /^\d+$/
|
891
|
+
# If the user was specified as a numeric UID, use it directly.
|
892
|
+
uid = user
|
893
|
+
else
|
894
|
+
# Otherwise attempt to look up the username to get a UID.
|
895
|
+
# Default to UID 0 if the username can't be found.
|
896
|
+
# TODO: Should we cache this info somewhere?
|
897
|
+
begin
|
898
|
+
pw = Etc.getpwnam(user)
|
899
|
+
uid = pw.uid
|
900
|
+
rescue ArgumentError
|
901
|
+
puts "Package requests user #{user}, but that user can't be found. Using UID 0."
|
902
|
+
uid = 0
|
903
|
+
end
|
904
|
+
end
|
905
|
+
|
906
|
+
uid.to_i
|
907
|
+
end
|
908
|
+
|
909
|
+
def self.lookup_gid(group)
|
910
|
+
gid = nil
|
911
|
+
if group =~ /^\d+$/
|
912
|
+
# If the group was specified as a numeric GID, use it directly.
|
913
|
+
gid = group
|
914
|
+
else
|
915
|
+
# Otherwise attempt to look up the group to get a GID. Default
|
916
|
+
# to GID 0 if the group can't be found.
|
917
|
+
# TODO: Should we cache this info somewhere?
|
918
|
+
begin
|
919
|
+
gr = Etc.getgrnam(group)
|
920
|
+
gid = gr.gid
|
921
|
+
rescue ArgumentError
|
922
|
+
puts "Package requests group #{group}, but that group can't be found. Using GID 0."
|
923
|
+
gid = 0
|
924
|
+
end
|
925
|
+
end
|
926
|
+
|
927
|
+
gid.to_i
|
928
|
+
end
|
929
|
+
|
930
|
+
def self.gethttp(uri)
|
931
|
+
if uri.scheme != 'http' && uri.scheme != 'https'
|
932
|
+
# It would be possible to add support for FTP and possibly
|
933
|
+
# other things if anyone cares
|
934
|
+
raise "Only http/https URIs are supported, got: '#{uri}'"
|
935
|
+
end
|
936
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
937
|
+
if uri.scheme == 'https'
|
938
|
+
# Eliminate the OpenSSL "using default DH parameters" warning
|
939
|
+
if File.exist?(File.join(CONFIGDIR, 'tpkg', 'dhparams'))
|
940
|
+
dh = OpenSSL::PKey::DH.new(IO.read(File.join(CONFIGDIR, 'tpkg', 'dhparams')))
|
941
|
+
Net::HTTP.ssl_context_accessor(:tmp_dh_callback)
|
942
|
+
http.tmp_dh_callback = proc { dh }
|
943
|
+
end
|
944
|
+
http.use_ssl = true
|
945
|
+
if File.exist?(File.join(CONFIGDIR, 'tpkg', 'ca.pem'))
|
946
|
+
http.ca_file = File.join(CONFIGDIR, 'tpkg', 'ca.pem')
|
947
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
948
|
+
elsif File.directory?(File.join(CONFIGDIR, 'tpkg', 'ca'))
|
949
|
+
http.ca_path = File.join(CONFIGDIR, 'tpkg', 'ca')
|
950
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
951
|
+
end
|
952
|
+
end
|
953
|
+
http.start
|
954
|
+
http
|
955
|
+
end
|
956
|
+
|
957
|
+
# foo
|
958
|
+
# foo=1.0
|
959
|
+
# foo=1.0=1
|
960
|
+
# foo-1.0-1.tpkg
|
961
|
+
def self.parse_request(request, installed_dir = nil)
|
962
|
+
# FIXME: Add support for <, <=, >, >=
|
963
|
+
req = {}
|
964
|
+
parts = request.split('=')
|
965
|
+
|
966
|
+
# upgrade/remove/query options should take package filenames
|
967
|
+
# First, look inside installed dir to see if we can find the request package. This is to support
|
968
|
+
# request that uses package filename rather than package name
|
969
|
+
if installed_dir && File.exists?(File.join(installed_dir, request))
|
970
|
+
metadata = Tpkg::metadata_from_package(File.join(installed_dir, request))
|
971
|
+
req[:name] = metadata[:name]
|
972
|
+
req[:minimum_version] = metadata[:version].to_s
|
973
|
+
req[:maximum_version] = metadata[:version].to_s
|
974
|
+
if metadata[:package_version] && !metadata[:package_version].to_s.empty?
|
975
|
+
req[:minimum_package_version] = metadata[:package_version].to_s
|
976
|
+
req[:maximum_package_version] = metadata[:package_version].to_s
|
977
|
+
end
|
978
|
+
elsif parts.length > 2 && parts[-2] =~ /^[\d\.]/ && parts[-1] =~ /^[\d\.]/
|
979
|
+
package_version = parts.pop
|
980
|
+
version = parts.pop
|
981
|
+
req[:name] = parts.join('-')
|
982
|
+
req[:minimum_version] = version
|
983
|
+
req[:maximum_version] = version
|
984
|
+
req[:minimum_package_version] = package_version
|
985
|
+
req[:maximum_package_version] = package_version
|
986
|
+
elsif parts.length > 1 && parts[-1] =~ /^[\d\.]/
|
987
|
+
version = parts.pop
|
988
|
+
req[:name] = parts.join('-')
|
989
|
+
req[:minimum_version] = version
|
990
|
+
req[:maximum_version] = version
|
991
|
+
else
|
992
|
+
req[:name] = parts.join('-')
|
993
|
+
end
|
994
|
+
req
|
995
|
+
end
|
996
|
+
|
997
|
+
# deploy_options is used for configuration the deployer. It is a map of option_names => option_values. Possible
|
998
|
+
# options are: use-ssh-key, deploy-as, worker-count, abort-on-fail
|
999
|
+
#
|
1000
|
+
# deploy_params is an array that holds the list of paramters that is used when invoking tpkg on to the remote
|
1001
|
+
# servers where we want to deploy to.
|
1002
|
+
#
|
1003
|
+
# servers is an array or a callback that list the remote servers where we want to deploy to
|
1004
|
+
def self.deploy(deploy_params, deploy_options, servers)
|
1005
|
+
deployer = Deployer.new(deploy_options)
|
1006
|
+
deployer.deploy(deploy_params, servers)
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
# Given a pid, check if it is running
|
1010
|
+
def self.process_running?(pid)
|
1011
|
+
return false if pid.nil? or pid == ""
|
1012
|
+
begin
|
1013
|
+
Process.kill(0, pid.to_i)
|
1014
|
+
rescue Errno::ESRCH
|
1015
|
+
return false
|
1016
|
+
rescue => e
|
1017
|
+
puts e
|
1018
|
+
return true
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
# Prompt user to confirm yes or no. Default to yes if user just hit enter without any input.
|
1023
|
+
def self.confirm
|
1024
|
+
while true
|
1025
|
+
print "Confirm? [Y/n] "
|
1026
|
+
response = $stdin.gets
|
1027
|
+
if response =~ /^n/i
|
1028
|
+
return false
|
1029
|
+
elsif response =~ /^y|^\s$/i
|
1030
|
+
return true
|
1031
|
+
end
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
def self.extract_tpkgxml(package_file)
|
1036
|
+
result = ""
|
1037
|
+
workdir = ""
|
1038
|
+
begin
|
1039
|
+
topleveldir = Tpkg::package_toplevel_directory(package_file)
|
1040
|
+
workdir = Tpkg::tempdir(topleveldir)
|
1041
|
+
system("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{find_tar} -C #{workdir} -xpf -")
|
1042
|
+
|
1043
|
+
if !File.exist?(File.join(workdir,"tpkg", "tpkg.xml"))
|
1044
|
+
raise "#{package_file} does not contain tpkg.xml"
|
1045
|
+
else
|
1046
|
+
File.open(File.join(workdir,"tpkg", "tpkg.xml"), "r") do | f |
|
1047
|
+
result = f.read
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
rescue
|
1051
|
+
puts "Failed to extract package."
|
1052
|
+
ensure
|
1053
|
+
FileUtils.rm_rf(workdir) if workdir
|
1054
|
+
end
|
1055
|
+
return result
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
#
|
1059
|
+
# Instance methods
|
1060
|
+
#
|
1061
|
+
|
1062
|
+
DEFAULT_BASE = '/home/t'
|
1063
|
+
|
1064
|
+
def initialize(options)
|
1065
|
+
# Options
|
1066
|
+
@base = options[:base]
|
1067
|
+
# An array of filenames or URLs which point to individual package files
|
1068
|
+
# or directories containing packages and extracted metadata.
|
1069
|
+
@sources = []
|
1070
|
+
if options[:sources]
|
1071
|
+
@sources = options[:sources]
|
1072
|
+
# Clean up any URI sources by ensuring they have a trailing slash
|
1073
|
+
# so that they are compatible with URI::join
|
1074
|
+
@sources.map! do |source|
|
1075
|
+
if !File.exist?(source) && source !~ %r{/$}
|
1076
|
+
source << '/'
|
1077
|
+
end
|
1078
|
+
source
|
1079
|
+
end
|
1080
|
+
end
|
1081
|
+
@report_server = nil
|
1082
|
+
if options[:report_server]
|
1083
|
+
@report_server = options[:report_server]
|
1084
|
+
end
|
1085
|
+
@lockforce = false
|
1086
|
+
if options.has_key?(:lockforce)
|
1087
|
+
@lockforce = options[:lockforce]
|
1088
|
+
end
|
1089
|
+
@force =false
|
1090
|
+
if options.has_key?(:force)
|
1091
|
+
@force = options[:force]
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
@file_system_root = '/' # Not sure if this needs to be more portable
|
1095
|
+
# This option is only intended for use by the test suite
|
1096
|
+
if options[:file_system_root]
|
1097
|
+
@file_system_root = options[:file_system_root]
|
1098
|
+
@base = File.join(@file_system_root, @base)
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
# Various external scripts that we run might need to adjust things for
|
1102
|
+
# relocatable packages based on the base directory. Set $TPKG_HOME so
|
1103
|
+
# those scripts know what base directory is being used.
|
1104
|
+
ENV['TPKG_HOME'] = @base
|
1105
|
+
|
1106
|
+
# Other instance variables
|
1107
|
+
@metadata = {}
|
1108
|
+
@available_packages = {}
|
1109
|
+
@available_native_packages = {}
|
1110
|
+
@var_directory = File.join(@base, 'var', 'tpkg')
|
1111
|
+
if !File.exist?(@var_directory)
|
1112
|
+
begin
|
1113
|
+
FileUtils.mkdir_p(@var_directory)
|
1114
|
+
rescue Errno::EACCES
|
1115
|
+
raise if Process.euid == 0
|
1116
|
+
rescue Errno::EIO => e
|
1117
|
+
if Tpkg::get_os =~ /Darwin/
|
1118
|
+
# Try to help our Mac OS X users, otherwise this could be
|
1119
|
+
# rather confusing.
|
1120
|
+
warn "\nNote: /home is controlled by the automounter by default on Mac OS X.\n" +
|
1121
|
+
"You'll either need to disable that in /etc/auto_master or configure\n" +
|
1122
|
+
"tpkg to use a different base via tpkg.conf.\n"
|
1123
|
+
end
|
1124
|
+
raise e
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
@installed_directory = File.join(@var_directory, 'installed')
|
1128
|
+
if !File.exist?(@installed_directory)
|
1129
|
+
begin
|
1130
|
+
FileUtils.mkdir_p(@installed_directory)
|
1131
|
+
rescue Errno::EACCES
|
1132
|
+
raise if Process.euid == 0
|
1133
|
+
end
|
1134
|
+
end
|
1135
|
+
@metadata_directory = File.join(@installed_directory, 'metadata')
|
1136
|
+
if !File.exist?(@metadata_directory)
|
1137
|
+
begin
|
1138
|
+
FileUtils.mkdir_p(@metadata_directory)
|
1139
|
+
rescue Errno::EACCES
|
1140
|
+
raise if Process.euid == 0
|
1141
|
+
end
|
1142
|
+
end
|
1143
|
+
@sources_directory = File.join(@var_directory, 'sources')
|
1144
|
+
if !File.exist?(@sources_directory)
|
1145
|
+
begin
|
1146
|
+
FileUtils.mkdir_p(@sources_directory)
|
1147
|
+
rescue Errno::EACCES
|
1148
|
+
raise if Process.euid == 0
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
@external_directory = File.join(@var_directory, 'externals')
|
1152
|
+
if !File.exist?(@external_directory)
|
1153
|
+
begin
|
1154
|
+
FileUtils.mkdir_p(@external_directory)
|
1155
|
+
rescue Errno::EACCES
|
1156
|
+
raise if Process.euid == 0
|
1157
|
+
end
|
1158
|
+
end
|
1159
|
+
@tmp_directory = File.join(@var_directory, 'tmp')
|
1160
|
+
if !File.exist?(@tmp_directory)
|
1161
|
+
begin
|
1162
|
+
FileUtils.mkdir_p(@tmp_directory)
|
1163
|
+
rescue Errno::EACCES
|
1164
|
+
raise if Process.euid == 0
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
@tar = Tpkg::find_tar
|
1168
|
+
@lock_directory = File.join(@var_directory, 'lock')
|
1169
|
+
@lock_pid_file = File.join(@lock_directory, 'pid')
|
1170
|
+
@locks = 0
|
1171
|
+
@installed_metadata = {}
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
def source_to_local_directory(source)
|
1175
|
+
source_as_directory = source.gsub(/[^a-zA-Z0-9]/, '')
|
1176
|
+
File.join(@sources_directory, source_as_directory)
|
1177
|
+
end
|
1178
|
+
|
1179
|
+
# One-time operations related to loading information about available
|
1180
|
+
# packages
|
1181
|
+
def prep_metadata
|
1182
|
+
if @metadata.empty?
|
1183
|
+
metadata = {}
|
1184
|
+
@sources.each do |source|
|
1185
|
+
if File.file?(source)
|
1186
|
+
metadata_yml = Tpkg::metadata_from_package(source)
|
1187
|
+
metadata_yml.source = source
|
1188
|
+
name = metadata_yml[:name]
|
1189
|
+
metadata[name] = [] if !metadata[name]
|
1190
|
+
metadata[name] << metadata_yml
|
1191
|
+
elsif File.directory?(source)
|
1192
|
+
if !File.exists?(File.join(source, 'metadata.yml'))
|
1193
|
+
warn "Warning: the source directory #{source} has no metadata.yml file. Try running tpkg -x #{source} first."
|
1194
|
+
next
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
metadata_contents = File.read(File.join(source, 'metadata.yml'))
|
1198
|
+
Metadata::get_pkgs_metadata_from_yml_doc(metadata_contents, metadata, source)
|
1199
|
+
else
|
1200
|
+
uri = http = localdate = remotedate = localdir = localpath = nil
|
1201
|
+
|
1202
|
+
['metadata.yml', 'metadata.xml'].each do | metadata_file |
|
1203
|
+
uri = URI.join(source, metadata_file)
|
1204
|
+
http = Tpkg::gethttp(uri)
|
1205
|
+
|
1206
|
+
# Calculate the path to the local copy of the metadata for this URI
|
1207
|
+
localdir = source_to_local_directory(source)
|
1208
|
+
localpath = File.join(localdir, metadata_file)
|
1209
|
+
localdate = nil
|
1210
|
+
if File.exist?(localpath)
|
1211
|
+
localdate = File.mtime(localpath)
|
1212
|
+
end
|
1213
|
+
|
1214
|
+
# For now, we always have to hit the repo once to determine if
|
1215
|
+
# it has metadata.yml or metadata.xml. In the future,
|
1216
|
+
# we will only support metadata.yml
|
1217
|
+
response = http.head(uri.path)
|
1218
|
+
case response
|
1219
|
+
when Net::HTTPSuccess
|
1220
|
+
remotedate = Time.httpdate(response['Date'])
|
1221
|
+
break
|
1222
|
+
else
|
1223
|
+
puts "Error fetching metadata from #{uri}: #{response.body}"
|
1224
|
+
next
|
1225
|
+
end
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
# Fetch the metadata if necessary
|
1229
|
+
metadata_contents = nil
|
1230
|
+
if !localdate || remotedate != localdate
|
1231
|
+
response = http.get(uri.path)
|
1232
|
+
case response
|
1233
|
+
when Net::HTTPSuccess
|
1234
|
+
metadata_contents = response.body
|
1235
|
+
remotedate = Time.httpdate(response['Date'])
|
1236
|
+
# Attempt to save a local copy, might not work if we're not
|
1237
|
+
# running with sufficient privileges
|
1238
|
+
begin
|
1239
|
+
if !File.exist?(localdir)
|
1240
|
+
FileUtils.mkdir_p(localdir)
|
1241
|
+
end
|
1242
|
+
File.open(localpath, 'w') do |file|
|
1243
|
+
file.puts(response.body)
|
1244
|
+
end
|
1245
|
+
File.utime(remotedate, remotedate, localpath)
|
1246
|
+
rescue Errno::EACCES
|
1247
|
+
raise if Process.euid == 0
|
1248
|
+
end
|
1249
|
+
else
|
1250
|
+
puts "Error fetching metadata from #{uri}: #{response.body}"
|
1251
|
+
response.error! # Throws an exception
|
1252
|
+
end
|
1253
|
+
else
|
1254
|
+
metadata_contents = IO.read(localpath)
|
1255
|
+
end
|
1256
|
+
|
1257
|
+
if uri.path =~ /yml/
|
1258
|
+
Metadata::get_pkgs_metadata_from_yml_doc(metadata_contents, metadata, source)
|
1259
|
+
else
|
1260
|
+
# At this stage we just break up the metadata.xml document into
|
1261
|
+
# per-package chunks and save them for further parsing later.
|
1262
|
+
# This allows us to parse the whole metadata.xml just once, and
|
1263
|
+
# saves us from having to further parse and convert the
|
1264
|
+
# per-package chunks until if/when they are needed.
|
1265
|
+
tpkg_metadata = REXML::Document.new(metadata_contents)
|
1266
|
+
tpkg_metadata.elements.each('/tpkg_metadata/tpkg') do |metadata_xml|
|
1267
|
+
name = metadata_xml.elements['name'].text
|
1268
|
+
metadata[name] = [] if !metadata[name]
|
1269
|
+
metadata[name] << Metadata.new(metadata_xml.to_s, 'xml', source)
|
1270
|
+
end
|
1271
|
+
end
|
1272
|
+
end
|
1273
|
+
end
|
1274
|
+
@metadata = metadata
|
1275
|
+
if @@debug
|
1276
|
+
@sources.each do |source|
|
1277
|
+
count = metadata.inject(0) do |memo,m|
|
1278
|
+
# metadata is a hash of pkgname => array of metadata
|
1279
|
+
# hashes
|
1280
|
+
# Thus m is a 2 element array of [pkgname, array of
|
1281
|
+
# metadata hashes] And thus m[1] is the array of
|
1282
|
+
# metadata hashes. And metadata hashes are themselves
|
1283
|
+
# a hash of XML metadata and source.
|
1284
|
+
memo + m[1].select{|mh| mh[:source] == source}.length
|
1285
|
+
end
|
1286
|
+
puts "Found #{count} packages from #{source}"
|
1287
|
+
end
|
1288
|
+
end
|
1289
|
+
end
|
1290
|
+
end
|
1291
|
+
|
1292
|
+
# Populate our list of available packages for a given package name
|
1293
|
+
def load_available_packages(name=nil)
|
1294
|
+
prep_metadata
|
1295
|
+
|
1296
|
+
if name
|
1297
|
+
if !@available_packages[name]
|
1298
|
+
packages = []
|
1299
|
+
if @metadata[name]
|
1300
|
+
@metadata[name].each do |metadata_obj|
|
1301
|
+
packages << { :metadata => metadata_obj,
|
1302
|
+
:source => metadata_obj.source }
|
1303
|
+
end
|
1304
|
+
end
|
1305
|
+
@available_packages[name] = packages
|
1306
|
+
|
1307
|
+
if @@debug
|
1308
|
+
puts "Loaded #{@available_packages[name].size} available packages for #{name}"
|
1309
|
+
end
|
1310
|
+
end
|
1311
|
+
else
|
1312
|
+
# Load all packages
|
1313
|
+
@metadata.each do |pkgname, metadata_objs|
|
1314
|
+
if !@available_packages[pkgname]
|
1315
|
+
packages = []
|
1316
|
+
metadata_objs.each do |metadata_obj|
|
1317
|
+
packages << { :metadata => metadata_obj,
|
1318
|
+
:source => metadata_obj.source }
|
1319
|
+
end
|
1320
|
+
@available_packages[pkgname] = packages
|
1321
|
+
end
|
1322
|
+
end
|
1323
|
+
end
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
# Used by load_available_native_packages to stuff all the info about a
|
1327
|
+
# native package into a hash to match the structure we pass around
|
1328
|
+
# internally for tpkgs
|
1329
|
+
def pkg_for_native_package(name, version, package_version, source)
|
1330
|
+
metadata = {}
|
1331
|
+
metadata[:name] = name
|
1332
|
+
metadata[:version] = version
|
1333
|
+
metadata[:package_version] = package_version if package_version
|
1334
|
+
pkg = { :metadata => metadata, :source => source }
|
1335
|
+
if source == :native_installed
|
1336
|
+
pkg[:prefer] = true
|
1337
|
+
end
|
1338
|
+
pkg
|
1339
|
+
end
|
1340
|
+
|
1341
|
+
def load_available_native_packages(pkgname)
|
1342
|
+
if !@available_native_packages[pkgname]
|
1343
|
+
native_packages = []
|
1344
|
+
if Tpkg::get_os =~ /RedHat|CentOS|Fedora/
|
1345
|
+
[ {:arg => 'installed', :header => 'Installed', :source => :native_installed},
|
1346
|
+
{:arg => 'available', :header => 'Available', :source => :native_available} ].each do |yum|
|
1347
|
+
puts "available_native_packages running 'yum list #{yum[:arg]} #{pkgname}'" if @@debug
|
1348
|
+
stderr_first_line = nil
|
1349
|
+
Open3.popen3("yum list #{yum[:arg]} #{pkgname}") do |stdin, stdout, stderr|
|
1350
|
+
stdin.close
|
1351
|
+
read_packages = false
|
1352
|
+
stdout.each_line do |line|
|
1353
|
+
if line =~ /#{yum[:header]} Packages/
|
1354
|
+
# Skip the header lines until we get to this line
|
1355
|
+
read_packages = true
|
1356
|
+
elsif read_packages
|
1357
|
+
name_and_arch, ver_and_release, repo = line.split
|
1358
|
+
# In the end we ignore the architecture. Anything that
|
1359
|
+
# shows up in yum should be installable on this box, and
|
1360
|
+
# the chance of a mismatch between facter's idea of the
|
1361
|
+
# architecture and RPM's idea is high. I.e. i386 vs i686
|
1362
|
+
# or i32e vs x86_64 or whatever.
|
1363
|
+
name, arch = name_and_arch.split('.')
|
1364
|
+
# This is prone to error, as both the version and release
|
1365
|
+
# (what we call package version) could contain '-', so
|
1366
|
+
# there's no reliable way to parse the combined value.
|
1367
|
+
# RPM can show them separately, but seemingly not yum.
|
1368
|
+
# We could use rpm to list installed packages, but we
|
1369
|
+
# have to use yum to get available packages so we're
|
1370
|
+
# stuck with the problem.
|
1371
|
+
verparts = ver_and_release.split('-')
|
1372
|
+
package_version = verparts.pop
|
1373
|
+
version = verparts.join('-')
|
1374
|
+
# Create the pkg structure
|
1375
|
+
pkg = pkg_for_native_package(name, version, package_version, yum[:source])
|
1376
|
+
native_packages << pkg
|
1377
|
+
end
|
1378
|
+
end
|
1379
|
+
stderr_first_line = stderr.gets
|
1380
|
+
end
|
1381
|
+
if !$?.success?
|
1382
|
+
# Ignore 'no matching packages', raise anything else
|
1383
|
+
if stderr_first_line != "Error: No matching Packages to list\n"
|
1384
|
+
raise "available_native_packages error running yum"
|
1385
|
+
end
|
1386
|
+
end
|
1387
|
+
end
|
1388
|
+
elsif Tpkg::get_os =~ /Debian|Ubuntu/
|
1389
|
+
# The default 'dpkg -l' format has an optional third column for
|
1390
|
+
# errors, which makes it hard to parse reliably.
|
1391
|
+
puts "available_native_packages running dpkg-query -W -f='${Package} ${Version} ${Status}\n' #{pkgname}" if @@debug
|
1392
|
+
stderr_first_line = nil
|
1393
|
+
Open3.popen3("dpkg-query -W -f='${Package} ${Version} ${Status}\n' #{pkgname}") do |stdin, stdout, stderr|
|
1394
|
+
stdin.close
|
1395
|
+
stdout.each_line do |line|
|
1396
|
+
name, debversion, status = line.split(' ', 3)
|
1397
|
+
# Seems to be Debian convention that if the package has a
|
1398
|
+
# package version you seperate that from the upstream version
|
1399
|
+
# with a hyphen.
|
1400
|
+
version = nil
|
1401
|
+
package_version = nil
|
1402
|
+
if debversion =~ /-/
|
1403
|
+
version, package_version = debversion.split('-', 2)
|
1404
|
+
else
|
1405
|
+
version = debversion
|
1406
|
+
end
|
1407
|
+
if status =~ /installed/
|
1408
|
+
pkg = pkg_for_native_package(name, version, package_version, :native_installed)
|
1409
|
+
native_packages << pkg
|
1410
|
+
end
|
1411
|
+
end
|
1412
|
+
stderr_first_line = stderr.gets
|
1413
|
+
end
|
1414
|
+
if !$?.success?
|
1415
|
+
# Ignore 'no matching packages', raise anything else
|
1416
|
+
if stderr_first_line !~ 'No packages found matching'
|
1417
|
+
raise "available_native_packages error running dpkg-query"
|
1418
|
+
end
|
1419
|
+
end
|
1420
|
+
puts "available_native_packages running 'apt-cache show #{pkgname}'" if @@debug
|
1421
|
+
IO.popen("apt-cache show #{pkgname}") do |pipe|
|
1422
|
+
name = nil
|
1423
|
+
version = nil
|
1424
|
+
package_version = nil
|
1425
|
+
pipe.each_line do |line|
|
1426
|
+
if line =~ /^Package: (.*)/
|
1427
|
+
name = $1
|
1428
|
+
version = nil
|
1429
|
+
package_version = nil
|
1430
|
+
elsif line =~ /^Version: (.*)/
|
1431
|
+
debversion = $1
|
1432
|
+
# Seems to be Debian convention that if the package has a
|
1433
|
+
# package version you seperate that from the upstream version
|
1434
|
+
# with a hyphen.
|
1435
|
+
if debversion =~ /-/
|
1436
|
+
version, package_version = debversion.split('-', 2)
|
1437
|
+
else
|
1438
|
+
version = debversion
|
1439
|
+
end
|
1440
|
+
pkg = pkg_for_native_package(name, version, package_version, :native_available)
|
1441
|
+
native_packages << pkg
|
1442
|
+
end
|
1443
|
+
end
|
1444
|
+
end
|
1445
|
+
if !$?.success?
|
1446
|
+
raise "available_native_packages error running apt-cache"
|
1447
|
+
end
|
1448
|
+
elsif Tpkg::get_os =~ /Solaris/
|
1449
|
+
# Example of pkginfo -x output:
|
1450
|
+
# SUNWzfsu ZFS (Usr)
|
1451
|
+
# (i386) 11.10.0,REV=2006.05.18.01.46
|
1452
|
+
puts "available_native_packages running 'pkginfo -x #{pkgname}'" if @@debug
|
1453
|
+
IO.popen("pkginfo -x #{pkgname}") do |pipe|
|
1454
|
+
name = nil
|
1455
|
+
version = nil
|
1456
|
+
package_version = nil
|
1457
|
+
pipe.each_line do |line|
|
1458
|
+
if line =~ /^\w/
|
1459
|
+
name = line.split(' ')
|
1460
|
+
version = nil
|
1461
|
+
package_version = nil
|
1462
|
+
else
|
1463
|
+
arch, solversion = line.split(' ')
|
1464
|
+
# Lots of Sun and some third party packages (including CSW)
|
1465
|
+
# seem to use this REV= convention in the version. I've
|
1466
|
+
# never seen it documented, but since it seems to be a
|
1467
|
+
# widely used convention we'll go with it.
|
1468
|
+
if solversion =~ /,REV=/
|
1469
|
+
version, package_version = solversion.split(',REV=')
|
1470
|
+
else
|
1471
|
+
version = solversion
|
1472
|
+
end
|
1473
|
+
pkg = pkg_for_native_package(name, version, package_version, :native_installed)
|
1474
|
+
native_packages << pkg
|
1475
|
+
end
|
1476
|
+
end
|
1477
|
+
end
|
1478
|
+
if !$?.success?
|
1479
|
+
raise "available_native_packages error running pkginfo"
|
1480
|
+
end
|
1481
|
+
if File.exist?('/opt/csw/bin/pkg-get')
|
1482
|
+
puts "available_native_packages running '/opt/csw/bin/pkg-get -a'" if @@debug
|
1483
|
+
IO.popen('/opt/csw/bin/pkg-get -a') do |pipe|
|
1484
|
+
pipe.each_line do |line|
|
1485
|
+
next if line =~ /^#/ # Skip comments
|
1486
|
+
name, solversion = line.split
|
1487
|
+
# pkg-get doesn't have an option to only show available
|
1488
|
+
# packages matching a specific name, so we have to look over
|
1489
|
+
# all available packages and pick out the ones that match.
|
1490
|
+
next if name != pkgname
|
1491
|
+
# Lots of Sun and some third party packages (including CSW)
|
1492
|
+
# seem to use this REV= convention in the version. I've
|
1493
|
+
# never seen it documented, but since it seems to be a
|
1494
|
+
# widely used convention we'll go with it.
|
1495
|
+
version = nil
|
1496
|
+
package_version = nil
|
1497
|
+
if solversion =~ /,REV=/
|
1498
|
+
version, package_version = solversion.split(',REV=')
|
1499
|
+
else
|
1500
|
+
version = solversion
|
1501
|
+
end
|
1502
|
+
pkg = pkg_for_native_package(name, version, package_version, :native_available)
|
1503
|
+
native_packages << pkg
|
1504
|
+
end
|
1505
|
+
end
|
1506
|
+
end
|
1507
|
+
elsif Tpkg::get_os =~ /FreeBSD/
|
1508
|
+
puts "available_native_packages running 'pkg_info #{pkgname}'" if @@debug
|
1509
|
+
IO.popen("pkg_info #{pkgname}") do |pipe|
|
1510
|
+
pipe.each_line do |line|
|
1511
|
+
name_and_version = line.split(' ', 3)
|
1512
|
+
nameparts = name_and_version.split('-')
|
1513
|
+
fbversion = nameparts.pop
|
1514
|
+
name = nameparts.join('-')
|
1515
|
+
# Seems to be FreeBSD convention that if the package has a
|
1516
|
+
# package version you seperate that from the upstream version
|
1517
|
+
# with an underscore.
|
1518
|
+
version = nil
|
1519
|
+
package_version = nil
|
1520
|
+
if fbversion =~ /_/
|
1521
|
+
version, package_version = fbversion.split('_', 2)
|
1522
|
+
else
|
1523
|
+
version = fbversion
|
1524
|
+
end
|
1525
|
+
pkg = pkg_for_native_package(name, version, package_version, :native_installed)
|
1526
|
+
package_version << pkg
|
1527
|
+
end
|
1528
|
+
end
|
1529
|
+
if !$?.success?
|
1530
|
+
raise "available_native_packages error running pkg_info"
|
1531
|
+
end
|
1532
|
+
# FIXME: FreeBSD available packages
|
1533
|
+
# We could either poke around in the ports tree (if installed), or
|
1534
|
+
# try to recreate the URL "pkg_add -r" would use and pull a
|
1535
|
+
# directory listing.
|
1536
|
+
elsif Tpkg::get_os =~ /Darwin/
|
1537
|
+
if File.exist?('/opt/local/bin/port')
|
1538
|
+
puts "available_native_packages running '/opt/local/bin/port installed #{pkgname}'" if @@debug
|
1539
|
+
IO.popen("/opt/local/bin/port installed #{pkgname}") do |pipe|
|
1540
|
+
pipe.each_line do |line|
|
1541
|
+
next if line =~ /The following ports are currently installed/
|
1542
|
+
next if line =~ /None of the specified ports are installed/
|
1543
|
+
next if line !~ /\(active\)/
|
1544
|
+
name, version = line.split(' ')
|
1545
|
+
version.sub!(/^@/, '')
|
1546
|
+
# Remove variant names
|
1547
|
+
version.sub!(/\+.*/, '')
|
1548
|
+
# Remove the _number that is always listed on installed ports,
|
1549
|
+
# presumably some sort of differentiator if multiple copies of
|
1550
|
+
# the same port version are installed.
|
1551
|
+
version.sub!(/_\d+$/, '')
|
1552
|
+
package_version = nil
|
1553
|
+
pkg = pkg_for_native_package(name, version, package_version, :native_installed)
|
1554
|
+
native_packages << pkg
|
1555
|
+
end
|
1556
|
+
end
|
1557
|
+
if !$?.success?
|
1558
|
+
raise "available_native_packages error running port"
|
1559
|
+
end
|
1560
|
+
puts "available_native_packages running '/opt/local/bin/port list #{pkgname}'" if @@debug
|
1561
|
+
IO.popen("/opt/local/bin/port list #{pkgname}") do |pipe|
|
1562
|
+
pipe.each_line do |line|
|
1563
|
+
name, version = line.split(' ')
|
1564
|
+
version.sub!(/^@/, '')
|
1565
|
+
package_version = nil
|
1566
|
+
pkg = pkg_for_native_package(name, version, package_version, :native_available)
|
1567
|
+
native_packages << pkg
|
1568
|
+
end
|
1569
|
+
end
|
1570
|
+
if !$?.success?
|
1571
|
+
raise "available_native_packages error running port"
|
1572
|
+
end
|
1573
|
+
else
|
1574
|
+
# Fink support would be nice
|
1575
|
+
raise "No supported native package tool available on #{Tpkg::get_os}"
|
1576
|
+
end
|
1577
|
+
else
|
1578
|
+
puts "Unknown value for OS: #{Tpkg::get_os}"
|
1579
|
+
end
|
1580
|
+
@available_native_packages[pkgname] = native_packages
|
1581
|
+
if @@debug
|
1582
|
+
nicount = native_packages.select{|pkg| pkg[:source] == :native_installed}.length
|
1583
|
+
nacount = native_packages.select{|pkg| pkg[:source] == :native_available}.length
|
1584
|
+
puts "Found #{nicount} installed native packages for #{pkgname}"
|
1585
|
+
puts "Found #{nacount} available native packages for #{pkgname}"
|
1586
|
+
end
|
1587
|
+
end
|
1588
|
+
end
|
1589
|
+
|
1590
|
+
# Returns an array of the tpkg.xml metadata for installed packages
|
1591
|
+
def metadata_for_installed_packages
|
1592
|
+
metadata = {}
|
1593
|
+
if File.directory?(@installed_directory)
|
1594
|
+
Dir.foreach(@installed_directory) do |entry|
|
1595
|
+
next if entry == '.' || entry == '..' || entry == 'metadata'
|
1596
|
+
# Check the timestamp on the file to see if it is new or has
|
1597
|
+
# changed since we last loaded data
|
1598
|
+
timestamp = File.mtime(File.join(@installed_directory, entry))
|
1599
|
+
if @installed_metadata[entry] &&
|
1600
|
+
timestamp == @installed_metadata[entry][:timestamp]
|
1601
|
+
puts "Using cached installed metadata for #{entry}" if @@debug
|
1602
|
+
metadata[entry] = @installed_metadata[entry]
|
1603
|
+
else
|
1604
|
+
puts "Loading installed metadata from disk for #{entry}" if @@debug
|
1605
|
+
# Check to see if we already have a saved copy of the metadata
|
1606
|
+
# Originally tpkg just stored a copy of the package file in
|
1607
|
+
# @installed_directory and we had to extract the metadata
|
1608
|
+
# from the package file every time we needed it. That was
|
1609
|
+
# determined to be too slow, so we now cache a copy of the
|
1610
|
+
# metadata separately. However we may encounter installs by
|
1611
|
+
# old copies of tpkg and need to extract and cache the
|
1612
|
+
# metadata.
|
1613
|
+
package_metadata_dir =
|
1614
|
+
File.join(@metadata_directory,
|
1615
|
+
File.basename(entry, File.extname(entry)))
|
1616
|
+
metadata_file = File.join(package_metadata_dir, "tpkg.yml")
|
1617
|
+
m = nil
|
1618
|
+
if File.exists?(metadata_file)
|
1619
|
+
metadata_text = File.read(metadata_file)
|
1620
|
+
m = Metadata.new(metadata_text, 'yml')
|
1621
|
+
elsif File.exists?(File.join(package_metadata_dir, "tpkg.xml"))
|
1622
|
+
metadata_text = File.read(File.join(package_metadata_dir, "tpkg.xml"))
|
1623
|
+
m = Metadata.new(metadata_text, 'xml')
|
1624
|
+
# No cached metadata found, we have to extract it ourselves
|
1625
|
+
# and save it for next time
|
1626
|
+
else
|
1627
|
+
m = Tpkg::metadata_from_package(
|
1628
|
+
File.join(@installed_directory, entry))
|
1629
|
+
begin
|
1630
|
+
FileUtils.mkdir_p(package_metadata_dir)
|
1631
|
+
File.open(metadata_file, "w") do |file|
|
1632
|
+
YAML::dump(m.hash, file)
|
1633
|
+
end
|
1634
|
+
rescue Errno::EACCES
|
1635
|
+
raise if Process.euid == 0
|
1636
|
+
end
|
1637
|
+
end
|
1638
|
+
metadata[entry] = { :timestamp => timestamp,
|
1639
|
+
:metadata => m }
|
1640
|
+
end
|
1641
|
+
end
|
1642
|
+
end
|
1643
|
+
@installed_metadata = metadata
|
1644
|
+
# FIXME: dup the array we return?
|
1645
|
+
@installed_metadata.collect { |im| im[1][:metadata] }
|
1646
|
+
end
|
1647
|
+
|
1648
|
+
# Convert metadata_for_installed_packages into pkg hashes
|
1649
|
+
def installed_packages
|
1650
|
+
instpkgs = []
|
1651
|
+
metadata_for_installed_packages.each do |metadata|
|
1652
|
+
instpkgs << { :metadata => metadata,
|
1653
|
+
:source => :currently_installed,
|
1654
|
+
# It seems reasonable for this to default to true
|
1655
|
+
:prefer => true }
|
1656
|
+
end
|
1657
|
+
instpkgs
|
1658
|
+
end
|
1659
|
+
|
1660
|
+
# Returns a hash of file_metadata for installed packages
|
1661
|
+
def file_metadata_for_installed_packages
|
1662
|
+
ret = {}
|
1663
|
+
|
1664
|
+
if File.directory?(@metadata_directory)
|
1665
|
+
Dir.foreach(@metadata_directory) do |entry|
|
1666
|
+
next if entry == '.' || entry == '..'
|
1667
|
+
if File.exists?(File.join(@metadata_directory, entry, "file_metadata.bin"))
|
1668
|
+
file = File.join(@metadata_directory, entry, "file_metadata.bin")
|
1669
|
+
file_metadata = FileMetadata.new(File.read(file), 'bin')
|
1670
|
+
elsif File.exists?(File.join(@metadata_directory, entry, "file_metadata.yml"))
|
1671
|
+
file = File.join(@metadata_directory, entry, "file_metadata.yml")
|
1672
|
+
file_metadata = FileMetadata.new(File.read(file), 'yml')
|
1673
|
+
elsif File.exists?(File.join(@metadata_directory, entry, "file_metadata.xml"))
|
1674
|
+
file = File.join(@metadata_directory, entry, "file_metadata.xml")
|
1675
|
+
file_metadata = FileMetadata.new(File.read(file), 'xml')
|
1676
|
+
end
|
1677
|
+
ret[file_metadata[:package_file]] = file_metadata
|
1678
|
+
end
|
1679
|
+
end
|
1680
|
+
ret
|
1681
|
+
end
|
1682
|
+
|
1683
|
+
# Returns an array of packages which meet the given requirement
|
1684
|
+
def available_packages_that_meet_requirement(req=nil)
|
1685
|
+
pkgs = []
|
1686
|
+
puts "avail_pkgs_that_meet_req checking for #{req.inspect}" if @@debug
|
1687
|
+
if req
|
1688
|
+
if req[:type] == :native
|
1689
|
+
load_available_native_packages(req[:name])
|
1690
|
+
@available_native_packages[req[:name]].each do |pkg|
|
1691
|
+
if Tpkg::package_meets_requirement?(pkg, req)
|
1692
|
+
pkgs << pkg
|
1693
|
+
end
|
1694
|
+
end
|
1695
|
+
else
|
1696
|
+
load_available_packages(req[:name])
|
1697
|
+
@available_packages[req[:name]].each do |pkg|
|
1698
|
+
if Tpkg::package_meets_requirement?(pkg, req)
|
1699
|
+
pkgs << pkg
|
1700
|
+
end
|
1701
|
+
end
|
1702
|
+
# There's a weird dicotomy here where @available_packages contains
|
1703
|
+
# available tpkg and native packages, and _installed_ native
|
1704
|
+
# packages, but not installed tpkgs. That's somewhat intentional,
|
1705
|
+
# as we don't want to cache the installed state since that might
|
1706
|
+
# change during a run. We probably should be consistent, and not
|
1707
|
+
# cache installed native packages either. However, we do have
|
1708
|
+
# some intelligent caching of the installed tpkg state which would
|
1709
|
+
# be hard to replicate for native packages, and this method gets
|
1710
|
+
# called a lot so re-running the native package query commands
|
1711
|
+
# frequently would not be acceptable. So maybe we have the right
|
1712
|
+
# design, and this just serves as a note that it is not obvious.
|
1713
|
+
pkgs.concat(installed_packages_that_meet_requirement(req))
|
1714
|
+
end
|
1715
|
+
else
|
1716
|
+
# We return everything available if given a nil requirement
|
1717
|
+
# We do not include native packages
|
1718
|
+
load_available_packages
|
1719
|
+
# @available_packages is a hash of pkgname => array of pkgs
|
1720
|
+
# Thus m is a 2 element array of [pkgname, array of pkgs]
|
1721
|
+
# And thus m[1] is the array of packages
|
1722
|
+
pkgs = @available_packages.collect{|m| m[1]}.flatten
|
1723
|
+
end
|
1724
|
+
pkgs
|
1725
|
+
end
|
1726
|
+
def installed_packages_that_meet_requirement(req=nil)
|
1727
|
+
pkgs = []
|
1728
|
+
if req && req[:type] == :native
|
1729
|
+
load_available_native_packages(req[:name])
|
1730
|
+
@available_native_packages[req[:name]].each do |pkg|
|
1731
|
+
if pkg[:source] == :native_installed &&
|
1732
|
+
Tpkg::package_meets_requirement?(pkg, req)
|
1733
|
+
pkgs << pkg
|
1734
|
+
end
|
1735
|
+
end
|
1736
|
+
else
|
1737
|
+
installed_packages.each do |pkg|
|
1738
|
+
if req
|
1739
|
+
if Tpkg::package_meets_requirement?(pkg, req)
|
1740
|
+
pkgs << pkg
|
1741
|
+
end
|
1742
|
+
else
|
1743
|
+
pkgs << pkg
|
1744
|
+
end
|
1745
|
+
end
|
1746
|
+
end
|
1747
|
+
pkgs
|
1748
|
+
end
|
1749
|
+
# Takes a files structure as returned by files_in_package. Inserts
|
1750
|
+
# a new entry in the structure with the combined relocatable and
|
1751
|
+
# non-relocatable file lists normalized to their full paths.
|
1752
|
+
def normalize_paths(files)
|
1753
|
+
files[:normalized] = []
|
1754
|
+
files[:root].each do |rootfile|
|
1755
|
+
files[:normalized] << File.join(@file_system_root, rootfile)
|
1756
|
+
end
|
1757
|
+
files[:reloc].each do |relocfile|
|
1758
|
+
files[:normalized] << File.join(@base, relocfile)
|
1759
|
+
end
|
1760
|
+
end
|
1761
|
+
def files_for_installed_packages(package_files=nil)
|
1762
|
+
files = {}
|
1763
|
+
if !package_files
|
1764
|
+
package_files = []
|
1765
|
+
metadata_for_installed_packages.each do |metadata|
|
1766
|
+
package_files << metadata[:filename]
|
1767
|
+
end
|
1768
|
+
end
|
1769
|
+
metadata_for_installed_packages.each do |metadata|
|
1770
|
+
package_file = metadata[:filename]
|
1771
|
+
if package_files.include?(package_file)
|
1772
|
+
fip = Tpkg::files_in_package(File.join(@installed_directory, package_file))
|
1773
|
+
normalize_paths(fip)
|
1774
|
+
fip[:metadata] = metadata
|
1775
|
+
files[package_file] = fip
|
1776
|
+
end
|
1777
|
+
end
|
1778
|
+
files
|
1779
|
+
end
|
1780
|
+
|
1781
|
+
# Returns the best solution that meets the given requirements. Some
|
1782
|
+
# or all packages may be optionally pre-selected and specified via the
|
1783
|
+
# packages parameter, otherwise packages are picked from the set of
|
1784
|
+
# available packages. The packages parameter is in the form of a hash
|
1785
|
+
# with package names as keys pointing to arrays of package specs (our
|
1786
|
+
# standard hash of package metadata and source). The return value
|
1787
|
+
# will be an array of package specs.
|
1788
|
+
MAX_POSSIBLE_SOLUTIONS_TO_CHECK = 10000
|
1789
|
+
def best_solution(requirements, packages, core_packages)
|
1790
|
+
# Dup objects passed to us so that resolve_dependencies is free to
|
1791
|
+
# change them without potentially messing up our caller
|
1792
|
+
result = resolve_dependencies(requirements.dup, packages.dup, core_packages.dup)
|
1793
|
+
if @@debug
|
1794
|
+
if result[:solution]
|
1795
|
+
puts "bestsol picks: #{result[:solution].inspect}" if @@debug
|
1796
|
+
else
|
1797
|
+
puts "bestsol checked #{result[:number_of_possible_solutions_checked]} possible solutions, none worked"
|
1798
|
+
end
|
1799
|
+
end
|
1800
|
+
result[:solution]
|
1801
|
+
end
|
1802
|
+
|
1803
|
+
# Recursive method used by best_solution
|
1804
|
+
def resolve_dependencies(requirements, packages, core_packages, number_of_possible_solutions_checked=0)
|
1805
|
+
# Make sure we have populated package lists for all requirements.
|
1806
|
+
# Filter the package lists against the requirements and
|
1807
|
+
# ensure we can at least satisfy the initial requirements.
|
1808
|
+
requirements.each do |req|
|
1809
|
+
if !packages[req[:name]]
|
1810
|
+
puts "resolvedeps initializing packages for #{req.inspect}" if @@debug
|
1811
|
+
packages[req[:name]] =
|
1812
|
+
available_packages_that_meet_requirement(req)
|
1813
|
+
else
|
1814
|
+
# Loop over packages and eliminate ones that don't work for
|
1815
|
+
# this requirement
|
1816
|
+
puts "resolvedeps filtering packages for #{req.inspect}" if @@debug
|
1817
|
+
packages[req[:name]] =
|
1818
|
+
packages[req[:name]].select do |pkg|
|
1819
|
+
# When this method is called recursively there might be a
|
1820
|
+
# nil entry inserted into packages by the sorting code
|
1821
|
+
# below. We need to skip those.
|
1822
|
+
if pkg != nil
|
1823
|
+
Tpkg::package_meets_requirement?(pkg, req)
|
1824
|
+
end
|
1825
|
+
end
|
1826
|
+
end
|
1827
|
+
if packages[req[:name]].empty?
|
1828
|
+
if @@debug
|
1829
|
+
puts "No packages matching #{req.inspect}"
|
1830
|
+
end
|
1831
|
+
return {:number_of_possible_solutions_checked => number_of_possible_solutions_checked}
|
1832
|
+
end
|
1833
|
+
end
|
1834
|
+
# Sort the packages
|
1835
|
+
packages.each do |pkgname, pkgs|
|
1836
|
+
pkgs.sort!(&SORT_PACKAGES)
|
1837
|
+
# Only currently installed packages are allowed to score 0.
|
1838
|
+
# Anything else can score 1 at best. This ensures
|
1839
|
+
# that we prefer the solution which leaves the most
|
1840
|
+
# currently installed packages alone.
|
1841
|
+
if pkgs[0][:source] != :currently_installed &&
|
1842
|
+
pkgs[0][:source] != :native_installed
|
1843
|
+
pkgs.unshift(nil)
|
1844
|
+
end
|
1845
|
+
end
|
1846
|
+
|
1847
|
+
if @@debug
|
1848
|
+
puts "Packages after initial population and filtering:"
|
1849
|
+
puts packages.inspect
|
1850
|
+
end
|
1851
|
+
|
1852
|
+
# Here's an example of the possible solution sets we should come
|
1853
|
+
# up with and the proper ordering. Sets with identical averages
|
1854
|
+
# are equivalent, the order they appear in does not matter.
|
1855
|
+
#
|
1856
|
+
# packages: [a0, a1, a2], [b0, b1, b2], [c0, c1, c2]
|
1857
|
+
# core_packages: a, b
|
1858
|
+
#
|
1859
|
+
# [a0, b0, c0] (core avg 0) (avg 0)
|
1860
|
+
# [a0, b0, c1] (avg .33)
|
1861
|
+
# [a0, b0, c2] (avg .66)
|
1862
|
+
# [a0, b1, c0] (core avg .5) (avg .33)
|
1863
|
+
# [a1, b0, c0]
|
1864
|
+
# [a0, b1, c1] (avg .66)
|
1865
|
+
# [a1, b0, c1]
|
1866
|
+
# [a0, b1, c2] (avg 1)
|
1867
|
+
# [a1, b0, c2]
|
1868
|
+
# [a1, b1, c0] (core avg 1) (avg .66)
|
1869
|
+
# [a0, b2, c0]
|
1870
|
+
# [a2, b0, c0]
|
1871
|
+
# [a1, b1, c1] (avg 1)
|
1872
|
+
# [a0, b2, c1]
|
1873
|
+
# [a2, b0, c1]
|
1874
|
+
# [a1, b1, c2] (avg 1.33)
|
1875
|
+
# [a0, b2, c2]
|
1876
|
+
# [a2, b0, c2]
|
1877
|
+
# [a1, b2, c0] (core avg 1.5) (avg 1)
|
1878
|
+
# [a2, b1, c0]
|
1879
|
+
# [a1, b2, c1] (avg 1.33)
|
1880
|
+
# [a2, b1, c1]
|
1881
|
+
# [a1, b2, c2] (avg 1.67)
|
1882
|
+
# [a2, b1, c2]
|
1883
|
+
# [a2, b2, c0] (core avg 2) (avg 1.33)
|
1884
|
+
# [a2, b2, c1] (avg 1.67)
|
1885
|
+
# [a2, b2, c2] (avg 2)
|
1886
|
+
|
1887
|
+
# Divide packages into core and non-core packages
|
1888
|
+
corepkgs = packages.reject{|pkgname, pkgs| !core_packages.include?(pkgname)}
|
1889
|
+
noncorepkgs = packages.reject{|pkgname, pkgs| core_packages.include?(pkgname)}
|
1890
|
+
|
1891
|
+
# Calculate total package depth, the sum of the lengths (or rather
|
1892
|
+
# the max array index) of each array of packages.
|
1893
|
+
coretotaldepth = corepkgs.inject(0) {|memo, pkgs| memo + pkgs[1].length - 1}
|
1894
|
+
noncoretotaldepth = noncorepkgs.inject(0) {|memo, pkgs| memo + pkgs[1].length - 1}
|
1895
|
+
if @@debug
|
1896
|
+
puts "resolvedeps coretotaldepth #{coretotaldepth}"
|
1897
|
+
puts "resolvedeps noncoretotaldepth #{noncoretotaldepth}"
|
1898
|
+
end
|
1899
|
+
|
1900
|
+
# First pass, combinations of core packages
|
1901
|
+
(0..coretotaldepth).each do |coredepth|
|
1902
|
+
puts "resolvedeps checking coredepth: #{coredepth}" if @@debug
|
1903
|
+
core_solutions = [{:remaining_coredepth => coredepth, :pkgs => []}]
|
1904
|
+
corepkgs.each do |pkgname, pkgs|
|
1905
|
+
puts "resolvedeps corepkg #{pkgname}: #{pkgs.inspect}" if @@debug
|
1906
|
+
new_core_solutions = []
|
1907
|
+
core_solutions.each do |core_solution|
|
1908
|
+
remaining_coredepth = core_solution[:remaining_coredepth]
|
1909
|
+
puts "resolvedeps :remaining_coredepth: #{remaining_coredepth}" if @@debug
|
1910
|
+
(0..[remaining_coredepth, pkgs.length-1].min).each do |corepkgdepth|
|
1911
|
+
puts "resolvedeps corepkgdepth: #{corepkgdepth}" if @@debug
|
1912
|
+
# We insert a nil entry in some situations (see the sort
|
1913
|
+
# step earlier), so skip nil entries in the pkgs array.
|
1914
|
+
if pkgs[corepkgdepth] != nil
|
1915
|
+
coresol = core_solution.dup
|
1916
|
+
# Hash#dup doesn't dup each key/value, so we need to
|
1917
|
+
# explicitly dup :pkgs so that each copy has an
|
1918
|
+
# independent array that we can modify.
|
1919
|
+
coresol[:pkgs] = core_solution[:pkgs].dup
|
1920
|
+
coresol[:remaining_coredepth] -= corepkgdepth
|
1921
|
+
coresol[:pkgs] << pkgs[corepkgdepth]
|
1922
|
+
new_core_solutions << coresol
|
1923
|
+
# If this is a complete combination of core packages then
|
1924
|
+
# proceed to the next step
|
1925
|
+
puts "resolvedeps coresol[:pkgs] #{coresol[:pkgs].inspect}" if @@debug
|
1926
|
+
if coresol[:pkgs].length == corepkgs.length
|
1927
|
+
puts "resolvedeps complete core pkg set: #{coresol.inspect}" if @@debug
|
1928
|
+
# Solutions with remaining depth are duplicates of
|
1929
|
+
# solutions we already checked at lower depth levels
|
1930
|
+
# I.e. at coredepth==0 we'd have:
|
1931
|
+
# {:pkgs=>{a0, b0}, :remaining_coredepth=0}
|
1932
|
+
# And at coredepth==1:
|
1933
|
+
# {:pkgs=>{a0,b0}, :remaining_coredepth=1}
|
1934
|
+
# Whereas at coredepth==1 this is new and needs to be checked:
|
1935
|
+
# {:pkgs=>{a1,b0}, :remaining_coredepth=0}
|
1936
|
+
if coresol[:remaining_coredepth] == 0
|
1937
|
+
# Second pass, add combinations of non-core packages
|
1938
|
+
if noncorepkgs.empty?
|
1939
|
+
puts "resolvedeps noncorepkgs empty, checking solution" if @@debug
|
1940
|
+
result = check_solution(coresol, requirements, packages, core_packages, number_of_possible_solutions_checked)
|
1941
|
+
if result[:solution]
|
1942
|
+
return result
|
1943
|
+
else
|
1944
|
+
number_of_possible_solutions_checked = result[:number_of_possible_solutions_checked]
|
1945
|
+
end
|
1946
|
+
else
|
1947
|
+
(0..noncoretotaldepth).each do |noncoredepth|
|
1948
|
+
puts "resolvedeps noncoredepth: #{noncoredepth}" if @@debug
|
1949
|
+
coresol[:remaining_noncoredepth] = noncoredepth
|
1950
|
+
solutions = [coresol]
|
1951
|
+
noncorepkgs.each do |ncpkgname, ncpkgs|
|
1952
|
+
puts "resolvedeps noncorepkg #{ncpkgname}: #{ncpkgs.inspect}" if @@debug
|
1953
|
+
new_solutions = []
|
1954
|
+
solutions.each do |solution|
|
1955
|
+
remaining_noncoredepth = solution[:remaining_noncoredepth]
|
1956
|
+
puts "resolvedeps :remaining_noncoredepth: #{remaining_noncoredepth}" if @@debug
|
1957
|
+
(0..[remaining_noncoredepth, ncpkgs.length-1].min).each do |ncpkgdepth|
|
1958
|
+
puts "resolvedeps ncpkgdepth: #{ncpkgdepth}" if @@debug
|
1959
|
+
# We insert a nil entry in some situations (see the sort
|
1960
|
+
# step earlier), so skip nil entries in the pkgs array.
|
1961
|
+
if ncpkgs[ncpkgdepth] != nil
|
1962
|
+
sol = solution.dup
|
1963
|
+
# Hash#dup doesn't dup each key/value, so we need to
|
1964
|
+
# explicitly dup :pkgs so that each copy has an
|
1965
|
+
# independent array that we can modify.
|
1966
|
+
sol[:pkgs] = solution[:pkgs].dup
|
1967
|
+
sol[:remaining_noncoredepth] -= ncpkgdepth
|
1968
|
+
sol[:pkgs] << ncpkgs[ncpkgdepth]
|
1969
|
+
new_solutions << sol
|
1970
|
+
# If this is a complete combination of packages then
|
1971
|
+
# proceed to the next step
|
1972
|
+
puts "resolvedeps sol[:pkgs] #{sol[:pkgs].inspect}" if @@debug
|
1973
|
+
if sol[:pkgs].length == packages.length
|
1974
|
+
puts "resolvedeps complete pkg set: #{sol.inspect}" if @@debug
|
1975
|
+
# Solutions with remaining depth are duplicates of
|
1976
|
+
# solutions we already checked at lower depth levels
|
1977
|
+
if sol[:remaining_noncoredepth] == 0
|
1978
|
+
result = check_solution(sol, requirements, packages, core_packages, number_of_possible_solutions_checked)
|
1979
|
+
if result[:solution]
|
1980
|
+
return result
|
1981
|
+
else
|
1982
|
+
number_of_possible_solutions_checked = result[:number_of_possible_solutions_checked]
|
1983
|
+
end
|
1984
|
+
end
|
1985
|
+
end
|
1986
|
+
end
|
1987
|
+
end
|
1988
|
+
end
|
1989
|
+
solutions = new_solutions
|
1990
|
+
end
|
1991
|
+
end
|
1992
|
+
end
|
1993
|
+
end
|
1994
|
+
end
|
1995
|
+
end
|
1996
|
+
end
|
1997
|
+
end
|
1998
|
+
core_solutions = new_core_solutions
|
1999
|
+
end
|
2000
|
+
end
|
2001
|
+
# No solutions found
|
2002
|
+
return {:number_of_possible_solutions_checked => number_of_possible_solutions_checked}
|
2003
|
+
end
|
2004
|
+
|
2005
|
+
# Used by resolve_dependencies
|
2006
|
+
def check_solution(solution, requirements, packages, core_packages, number_of_possible_solutions_checked)
|
2007
|
+
number_of_possible_solutions_checked += 1
|
2008
|
+
# Probably should give the user a way to override this
|
2009
|
+
if number_of_possible_solutions_checked > MAX_POSSIBLE_SOLUTIONS_TO_CHECK
|
2010
|
+
raise "Checked #{MAX_POSSIBLE_SOLUTIONS_TO_CHECK} possible solutions to requirements and dependencies, no solution found"
|
2011
|
+
end
|
2012
|
+
|
2013
|
+
if @@debug
|
2014
|
+
puts "checksol checking #{solution.inspect}"
|
2015
|
+
end
|
2016
|
+
|
2017
|
+
# Extract dependencies from each package in the solution
|
2018
|
+
newreqs = []
|
2019
|
+
solution[:pkgs].each do |pkg|
|
2020
|
+
puts "checksol pkg #{pkg.inspect}" if @@debug
|
2021
|
+
if pkg[:metadata][:dependencies]
|
2022
|
+
pkg[:metadata][:dependencies].each do |depreq|
|
2023
|
+
if !requirements.include?(depreq) && !newreqs.include?(depreq)
|
2024
|
+
puts "checksol new depreq #{depreq.inspect}" if @@debug
|
2025
|
+
newreqs << depreq
|
2026
|
+
end
|
2027
|
+
end
|
2028
|
+
end
|
2029
|
+
end
|
2030
|
+
|
2031
|
+
if newreqs.empty?
|
2032
|
+
# No additional requirements, this is a complete solution
|
2033
|
+
puts "checksol no newreqs, complete solution" if @@debug
|
2034
|
+
return {:solution => solution[:pkgs]}
|
2035
|
+
else
|
2036
|
+
newreqs_that_need_packages = []
|
2037
|
+
newreqs.each do |newreq|
|
2038
|
+
puts "checksol checking newreq: #{newreq.inspect}" if @@debug
|
2039
|
+
if packages[newreq[:name]]
|
2040
|
+
pkg = solution[:pkgs].find{|solpkg| solpkg[:metadata][:name] == newreq[:name]}
|
2041
|
+
puts "checksol newreq pkg: #{pkg.inspect}" if @@debug
|
2042
|
+
if Tpkg::package_meets_requirement?(pkg, newreq)
|
2043
|
+
# No change to solution needed
|
2044
|
+
else
|
2045
|
+
# Solution no longer works
|
2046
|
+
puts "checksol solution no longer works" if @@debug
|
2047
|
+
return {:number_of_possible_solutions_checked => number_of_possible_solutions_checked}
|
2048
|
+
end
|
2049
|
+
else
|
2050
|
+
puts "checksol newreq needs packages" if @@debug
|
2051
|
+
newreqs_that_need_packages << newreq
|
2052
|
+
end
|
2053
|
+
end
|
2054
|
+
if newreqs_that_need_packages.empty?
|
2055
|
+
# None of the new requirements changed the solution, so the solution is complete
|
2056
|
+
puts "checksol no newreqs that need packages, complete solution" if @@debug
|
2057
|
+
return {:solution => solution[:pkgs]}
|
2058
|
+
else
|
2059
|
+
puts "checksol newreqs need packages, calling resolvedeps" if @@debug
|
2060
|
+
result = resolve_dependencies(requirements+newreqs_that_need_packages, packages.dup, core_packages, number_of_possible_solutions_checked)
|
2061
|
+
if result[:solution]
|
2062
|
+
return result
|
2063
|
+
else
|
2064
|
+
number_of_possible_solutions_checked = result[:number_of_possible_solutions_checked]
|
2065
|
+
end
|
2066
|
+
end
|
2067
|
+
end
|
2068
|
+
return {:number_of_possible_solutions_checked => number_of_possible_solutions_checked}
|
2069
|
+
end
|
2070
|
+
|
2071
|
+
def download(source, path, downloaddir = nil)
|
2072
|
+
http = Tpkg::gethttp(URI.parse(source))
|
2073
|
+
localdir = source_to_local_directory(source)
|
2074
|
+
localpath = File.join(localdir, File.basename(path))
|
2075
|
+
|
2076
|
+
# Don't download again if file is already there from previous installation
|
2077
|
+
# and still has valid checksum
|
2078
|
+
if File.file?(localpath)
|
2079
|
+
begin
|
2080
|
+
Tpkg::verify_package_checksum(localpath)
|
2081
|
+
return localpath
|
2082
|
+
rescue RuntimeError, NoMethodError
|
2083
|
+
# Previous download is bad (which can happen for a variety of
|
2084
|
+
# reasons like an interrupted download or a bad package on the
|
2085
|
+
# server). Delete it and we'll try to grab it again.
|
2086
|
+
File.delete(localpath)
|
2087
|
+
end
|
2088
|
+
else
|
2089
|
+
# If downloaddir is specified, then download to that directory. Otherwise,
|
2090
|
+
# download to default source directory
|
2091
|
+
localdir = downloaddir || localdir
|
2092
|
+
if !File.exist?(localdir)
|
2093
|
+
FileUtils.mkdir_p(localdir)
|
2094
|
+
end
|
2095
|
+
localpath = File.join(localdir, File.basename(path))
|
2096
|
+
end
|
2097
|
+
uri = URI.join(source, path)
|
2098
|
+
tmpfile = Tempfile.new(File.basename(localpath), File.dirname(localpath))
|
2099
|
+
http.request_get(uri.path) do |response|
|
2100
|
+
# Package files can be quite large, so we transfer the package to a
|
2101
|
+
# local file in chunks
|
2102
|
+
response.read_body do |chunk|
|
2103
|
+
tmpfile.write(chunk)
|
2104
|
+
end
|
2105
|
+
remotedate = Time.httpdate(response['Date'])
|
2106
|
+
File.utime(remotedate, remotedate, tmpfile.path)
|
2107
|
+
end
|
2108
|
+
tmpfile.close
|
2109
|
+
|
2110
|
+
begin
|
2111
|
+
Tpkg::verify_package_checksum(tmpfile.path)
|
2112
|
+
File.chmod(0644, tmpfile.path)
|
2113
|
+
File.rename(tmpfile.path, localpath)
|
2114
|
+
rescue
|
2115
|
+
raise "Unable to download and/or verify the package."
|
2116
|
+
end
|
2117
|
+
|
2118
|
+
localpath
|
2119
|
+
end
|
2120
|
+
|
2121
|
+
# Given a package's metadata return a hash of init scripts in the
|
2122
|
+
# package and the entry for that file from the metadata
|
2123
|
+
def init_scripts(metadata)
|
2124
|
+
init_scripts = {}
|
2125
|
+
# don't do anything unless we have to
|
2126
|
+
unless metadata[:files] && metadata[:files][:files]
|
2127
|
+
return init_scripts
|
2128
|
+
end
|
2129
|
+
metadata[:files][:files].each do |tpkgfile|
|
2130
|
+
if tpkgfile[:init]
|
2131
|
+
tpkg_path = tpkgfile[:path]
|
2132
|
+
installed_path = nil
|
2133
|
+
if tpkg_path[0,1] == File::SEPARATOR
|
2134
|
+
installed_path = File.join(@file_system_root, tpkg_path)
|
2135
|
+
else
|
2136
|
+
installed_path = File.join(@base, tpkg_path)
|
2137
|
+
end
|
2138
|
+
init_scripts[installed_path] = tpkgfile
|
2139
|
+
end
|
2140
|
+
end
|
2141
|
+
init_scripts
|
2142
|
+
end
|
2143
|
+
|
2144
|
+
# Given a package's metadata return a hash of init scripts in the
|
2145
|
+
# package and where they need to be linked to on the system
|
2146
|
+
def init_links(metadata)
|
2147
|
+
links = {}
|
2148
|
+
init_scripts(metadata).each do |installed_path, tpkgfile|
|
2149
|
+
# SysV-style init
|
2150
|
+
if Tpkg::get_os =~ /RedHat|CentOS|Fedora/ ||
|
2151
|
+
Tpkg::get_os =~ /Debian|Ubuntu/ ||
|
2152
|
+
Tpkg::get_os =~ /Solaris/
|
2153
|
+
start = '99'
|
2154
|
+
if tpkgfile[:init][:start]
|
2155
|
+
start = tpkgfile[:init][:start]
|
2156
|
+
end
|
2157
|
+
levels = nil
|
2158
|
+
if Tpkg::get_os =~ /RedHat|CentOS|Fedora/ ||
|
2159
|
+
Tpkg::get_os =~ /Debian|Ubuntu/
|
2160
|
+
levels = ['2', '3', '4', '5']
|
2161
|
+
elsif Tpkg::get_os =~ /Solaris/
|
2162
|
+
levels = ['2', '3']
|
2163
|
+
end
|
2164
|
+
if tpkgfile[:init][:levels]
|
2165
|
+
levels = tpkgfile[:init][:levels]
|
2166
|
+
end
|
2167
|
+
init_directory = nil
|
2168
|
+
if Tpkg::get_os =~ /RedHat|CentOS|Fedora/
|
2169
|
+
init_directory = File.join(@file_system_root, 'etc', 'rc.d')
|
2170
|
+
elsif Tpkg::get_os =~ /Debian|Ubuntu/ ||
|
2171
|
+
Tpkg::get_os =~ /Solaris/
|
2172
|
+
init_directory = File.join(@file_system_root, 'etc')
|
2173
|
+
end
|
2174
|
+
levels.to_s.each do |level|
|
2175
|
+
links[File.join(init_directory, "rc#{level}.d", 'S' + start.to_s + File.basename(installed_path))] = installed_path
|
2176
|
+
end
|
2177
|
+
elsif Tpkg::get_os =~ /FreeBSD/
|
2178
|
+
init_directory = File.join(@file_system_root, 'usr', 'local', 'etc', 'rc.d')
|
2179
|
+
if tpkgfile[:init][:levels] && tpkgfile[:init][:levels].empty?
|
2180
|
+
# User doesn't want the init script linked in to auto-start
|
2181
|
+
else
|
2182
|
+
links[File.join(init_directory, File.basename(installed_path))] = installed_path
|
2183
|
+
end
|
2184
|
+
else
|
2185
|
+
raise "No init script support for #{Tpkg::get_os}"
|
2186
|
+
end
|
2187
|
+
end
|
2188
|
+
links
|
2189
|
+
end
|
2190
|
+
|
2191
|
+
# Given a package's metadata return a hash of crontabs in the
|
2192
|
+
# package and where they need to be installed on the system
|
2193
|
+
def crontab_destinations(metadata)
|
2194
|
+
destinations = {}
|
2195
|
+
|
2196
|
+
# Don't do anything unless we have to
|
2197
|
+
unless metadata[:files] && metadata[:files][:files]
|
2198
|
+
return destinations
|
2199
|
+
end
|
2200
|
+
|
2201
|
+
metadata[:files][:files].each do |tpkgfile|
|
2202
|
+
if tpkgfile[:crontab]
|
2203
|
+
tpkg_path = tpkgfile[:path]
|
2204
|
+
installed_path = nil
|
2205
|
+
if tpkg_path[0,1] == File::SEPARATOR
|
2206
|
+
installed_path = File.join(@file_system_root, tpkg_path)
|
2207
|
+
else
|
2208
|
+
installed_path = File.join(@base, tpkg_path)
|
2209
|
+
end
|
2210
|
+
destinations[installed_path] = {}
|
2211
|
+
|
2212
|
+
# Decide whether we're going to add the file to a per-user
|
2213
|
+
# crontab or link it into a directory of misc. crontabs. If the
|
2214
|
+
# system only supports per-user crontabs we have to go the
|
2215
|
+
# per-user route. If the system supports both we decide based on
|
2216
|
+
# whether the package specifies a user for the crontab.
|
2217
|
+
# Systems that only support per-user style
|
2218
|
+
if Tpkg::get_os =~ /FreeBSD/ ||
|
2219
|
+
Tpkg::get_os =~ /Solaris/ ||
|
2220
|
+
Tpkg::get_os =~ /Darwin/
|
2221
|
+
if tpkgfile[:crontab][:user]
|
2222
|
+
user = tpkgfile[:crontab][:user]
|
2223
|
+
if Tpkg::get_os =~ /FreeBSD/
|
2224
|
+
destinations[installed_path][:file] = File.join(@file_system_root, 'var', 'cron', 'tabs', user)
|
2225
|
+
elsif Tpkg::get_os =~ /Solaris/
|
2226
|
+
destinations[installed_path][:file] = File.join(@file_system_root, 'var', 'spool', 'cron', 'crontabs', user)
|
2227
|
+
elsif Tpkg::get_os =~ /Darwin/
|
2228
|
+
destinations[installed_path][:file] = File.join(@file_system_root, 'usr', 'lib', 'cron', 'tabs', user)
|
2229
|
+
end
|
2230
|
+
else
|
2231
|
+
raise "No user specified for crontab in #{metadata[:filename]}"
|
2232
|
+
end
|
2233
|
+
# Systems that support cron.d style
|
2234
|
+
elsif Tpkg::get_os =~ /RedHat|CentOS|Fedora/ ||
|
2235
|
+
Tpkg::get_os =~ /Debian|Ubuntu/
|
2236
|
+
# If a user is specified go the per-user route
|
2237
|
+
if tpkgfile[:crontab][:user]
|
2238
|
+
user = tpkgfile[:crontab][:user]
|
2239
|
+
if Tpkg::get_os =~ /RedHat|CentOS|Fedora/
|
2240
|
+
destinations[installed_path][:file] = File.join(@file_system_root, 'var', 'spool', 'cron', user)
|
2241
|
+
elsif Tpkg::get_os =~ /Debian|Ubuntu/
|
2242
|
+
destinations[installed_path][:file] = File.join(@file_system_root, 'var', 'spool', 'cron', 'crontabs', user)
|
2243
|
+
end
|
2244
|
+
# Otherwise go the cron.d route
|
2245
|
+
else
|
2246
|
+
destinations[installed_path][:link] = File.join(@file_system_root, 'etc', 'cron.d', File.basename(installed_path))
|
2247
|
+
end
|
2248
|
+
else
|
2249
|
+
raise "No crontab support for #{Tpkg::get_os}"
|
2250
|
+
end
|
2251
|
+
end
|
2252
|
+
end
|
2253
|
+
destinations
|
2254
|
+
end
|
2255
|
+
|
2256
|
+
def run_external(pkgfile, operation, name, data)
|
2257
|
+
externalpath = File.join(@external_directory, name)
|
2258
|
+
if !File.executable?(externalpath)
|
2259
|
+
raise "External #{externalpath} does not exist or is not executable"
|
2260
|
+
end
|
2261
|
+
case operation
|
2262
|
+
when :install
|
2263
|
+
IO.popen("#{externalpath} '#{pkgfile}' install", 'w') do |pipe|
|
2264
|
+
pipe.write(data)
|
2265
|
+
end
|
2266
|
+
when :remove
|
2267
|
+
IO.popen("#{externalpath} '#{pkgfile}' remove", 'w') do |pipe|
|
2268
|
+
pipe.write(data)
|
2269
|
+
end
|
2270
|
+
else
|
2271
|
+
raise "Bug, unknown external operation #{operation}"
|
2272
|
+
end
|
2273
|
+
end
|
2274
|
+
|
2275
|
+
# Unpack the files from a package into place, decrypt as necessary, set
|
2276
|
+
# permissions and ownership, etc. Does not check for conflicting
|
2277
|
+
# files or packages, etc. Those checks (if desired) must be done before
|
2278
|
+
# calling this method.
|
2279
|
+
def unpack(package_file, passphrase=nil, options={})
|
2280
|
+
ret_val = 0
|
2281
|
+
metadata = Tpkg::metadata_from_package(package_file)
|
2282
|
+
|
2283
|
+
# Unpack files in a temporary directory
|
2284
|
+
# I'd prefer to unpack on the fly so that the user doesn't need to
|
2285
|
+
# have disk space to hold three copies of the package (the package
|
2286
|
+
# file itself, this temporary unpack, and the final copy of the
|
2287
|
+
# files). However, I haven't figured out a way to get that to work,
|
2288
|
+
# since we need to strip several layers of directories out of the
|
2289
|
+
# directory structure in the package.
|
2290
|
+
topleveldir = Tpkg::package_toplevel_directory(package_file)
|
2291
|
+
workdir = Tpkg::tempdir(topleveldir, @tmp_directory)
|
2292
|
+
system("#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{@tar} -C #{workdir} -xpf -")
|
2293
|
+
files_info = {} # store perms, uid, gid, etc. for files
|
2294
|
+
checksums_of_decrypted_files = {}
|
2295
|
+
root_dir = File.join(workdir, 'tpkg', 'root')
|
2296
|
+
reloc_dir = File.join(workdir, 'tpkg', 'reloc')
|
2297
|
+
rel_root_dir = File.join('tpkg', 'root')
|
2298
|
+
rel_reloc_dir = File.join('tpkg', 'reloc')
|
2299
|
+
|
2300
|
+
# Get list of conflicting files/directories & store their perm/ownership. That way, we can
|
2301
|
+
# set them to the correct values later on in order to preserve them.
|
2302
|
+
# TODO: verify this command works on all platforms
|
2303
|
+
files = `#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{@tar} -tf -`
|
2304
|
+
files = files.split("\n")
|
2305
|
+
conflicting_files = {}
|
2306
|
+
files.each do | file |
|
2307
|
+
if file =~ /^#{rel_root_dir}/
|
2308
|
+
possible_conflicting_file = "#{@file_system_root}/#{file[rel_root_dir.length ..-1]}"
|
2309
|
+
elsif file =~ /^#{rel_reloc_dir}/
|
2310
|
+
possible_conflicting_file = "#{@base}/#{file[rel_reloc_dir.length + 1..-1]}"
|
2311
|
+
end
|
2312
|
+
if possible_conflicting_file && (File.exists?(possible_conflicting_file) && !File.symlink?(possible_conflicting_file))
|
2313
|
+
conflicting_files[File.join(workdir, file)] = File.stat(possible_conflicting_file)
|
2314
|
+
end
|
2315
|
+
end
|
2316
|
+
|
2317
|
+
# Run preinstall script
|
2318
|
+
if File.exist?(File.join(workdir, 'tpkg', 'preinstall'))
|
2319
|
+
pwd = Dir.pwd
|
2320
|
+
# chdir into the working directory so that the user can specify a
|
2321
|
+
# relative path to their file/script.
|
2322
|
+
Dir.chdir(File.join(workdir, 'tpkg'))
|
2323
|
+
|
2324
|
+
# Warn the user about non-executable files, as system will just
|
2325
|
+
# silently fail and exit if that's the case.
|
2326
|
+
if !File.executable?(File.join(workdir, 'tpkg', 'preinstall'))
|
2327
|
+
warn "Warning: preinstall script for #{File.basename(package_file)} is not executable, execution will likely fail"
|
2328
|
+
end
|
2329
|
+
if @force
|
2330
|
+
system(File.join(workdir, 'tpkg', 'preinstall')) || warn("Warning: preinstall for #{File.basename(package_file)} failed with exit value #{$?.exitstatus}")
|
2331
|
+
else
|
2332
|
+
system(File.join(workdir, 'tpkg', 'preinstall')) || raise("Error: preinstall for #{File.basename(package_file)} failed with exit value #{$?.exitstatus}")
|
2333
|
+
end
|
2334
|
+
# Switch back to our previous directory
|
2335
|
+
Dir.chdir(pwd)
|
2336
|
+
end
|
2337
|
+
|
2338
|
+
# Run any externals
|
2339
|
+
metadata[:externals].each do |external|
|
2340
|
+
# If the external references a datafile or datascript then read/run it
|
2341
|
+
# now that we've unpacked the package contents and have the file/script
|
2342
|
+
# available. This will get us the data for the external.
|
2343
|
+
if external[:datafile] || external[:datascript]
|
2344
|
+
pwd = Dir.pwd
|
2345
|
+
# chdir into the working directory so that the user can specify a
|
2346
|
+
# relative path to their file/script.
|
2347
|
+
Dir.chdir(File.join(workdir, 'tpkg'))
|
2348
|
+
if external[:datafile]
|
2349
|
+
# Read the file
|
2350
|
+
external[:data] = IO.read(external[:datafile])
|
2351
|
+
# Drop the datafile key so that we don't waste time re-reading the
|
2352
|
+
# datafile again in the future.
|
2353
|
+
external.delete(:datafile)
|
2354
|
+
elsif external[:datascript]
|
2355
|
+
# Run the script
|
2356
|
+
IO.popen(external[:datascript]) do |pipe|
|
2357
|
+
external[:data] = pipe.read
|
2358
|
+
end
|
2359
|
+
# Drop the datascript key so that we don't waste time re-running the
|
2360
|
+
# datascript again in the future.
|
2361
|
+
external.delete(:datascript)
|
2362
|
+
end
|
2363
|
+
# Switch back to our previous directory
|
2364
|
+
Dir.chdir(pwd)
|
2365
|
+
end
|
2366
|
+
if !options[:externals_to_skip] || !options[:externals_to_skip].include?(external)
|
2367
|
+
run_external(metadata[:filename], :install, external[:name], external[:data])
|
2368
|
+
end
|
2369
|
+
end if metadata[:externals]
|
2370
|
+
|
2371
|
+
# Since we're stuck with unpacking to a temporary folder take
|
2372
|
+
# advantage of that to handle permissions, ownership and decryption
|
2373
|
+
# tasks before moving the files into their final location.
|
2374
|
+
|
2375
|
+
# Handle any default permissions and ownership
|
2376
|
+
default_uid = 0
|
2377
|
+
default_gid = 0
|
2378
|
+
default_perms = nil
|
2379
|
+
|
2380
|
+
if metadata[:files] && metadata[:files][:file_defaults]
|
2381
|
+
if metadata[:files][:file_defaults][:posix]
|
2382
|
+
if metadata[:files][:file_defaults][:posix][:owner]
|
2383
|
+
default_uid = Tpkg::lookup_uid(metadata[:files][:file_defaults][:posix][:owner])
|
2384
|
+
end
|
2385
|
+
if metadata[:files][:file_defaults][:posix][:group]
|
2386
|
+
default_gid = Tpkg::lookup_gid(metadata[:files][:file_defaults][:posix][:group])
|
2387
|
+
end
|
2388
|
+
if metadata[:files][:file_defaults][:posix][:perms]
|
2389
|
+
default_perms = metadata[:files][:file_defaults][:posix][:perms]
|
2390
|
+
end
|
2391
|
+
end
|
2392
|
+
end
|
2393
|
+
|
2394
|
+
# Set default dir uid/gid to be same as for file.
|
2395
|
+
default_dir_uid = default_uid
|
2396
|
+
default_dir_gid = default_gid
|
2397
|
+
default_dir_perms = 0755
|
2398
|
+
|
2399
|
+
if metadata[:files] && metadata[:files][:dir_defaults]
|
2400
|
+
if metadata[:files][:dir_defaults][:posix]
|
2401
|
+
if metadata[:files][:dir_defaults][:posix][:owner]
|
2402
|
+
default_dir_uid = Tpkg::lookup_uid(metadata[:files][:dir_defaults][:posix][:owner])
|
2403
|
+
end
|
2404
|
+
if metadata[:files][:dir_defaults][:posix][:group]
|
2405
|
+
default_dir_gid = Tpkg::lookup_gid(metadata[:files][:dir_defaults][:posix][:group])
|
2406
|
+
end
|
2407
|
+
if metadata[:files][:dir_defaults][:posix][:perms]
|
2408
|
+
default_dir_perms = metadata[:files][:dir_defaults][:posix][:perms]
|
2409
|
+
end
|
2410
|
+
end
|
2411
|
+
end
|
2412
|
+
|
2413
|
+
Find.find(root_dir, reloc_dir) do |f|
|
2414
|
+
# If the package doesn't contain either of the top level
|
2415
|
+
# directories we need to skip them, find will pass them to us
|
2416
|
+
# even if they don't exist.
|
2417
|
+
next if !File.exist?(f)
|
2418
|
+
|
2419
|
+
begin
|
2420
|
+
if File.directory?(f)
|
2421
|
+
File.chown(default_dir_uid, default_dir_gid, f)
|
2422
|
+
else
|
2423
|
+
File.chown(default_uid, default_gid, f)
|
2424
|
+
end
|
2425
|
+
rescue Errno::EPERM
|
2426
|
+
raise if Process.euid == 0
|
2427
|
+
end
|
2428
|
+
if File.file?(f) && !File.symlink?(f)
|
2429
|
+
if default_perms
|
2430
|
+
File.chmod(default_perms, f)
|
2431
|
+
end
|
2432
|
+
elsif File.directory?(f) && !File.symlink?(f)
|
2433
|
+
File.chmod(default_dir_perms, f)
|
2434
|
+
end
|
2435
|
+
end
|
2436
|
+
|
2437
|
+
# Reset the permission/ownership of the conflicting files as how they were before.
|
2438
|
+
# This needs to be done after the default permission/ownership is applied, but before
|
2439
|
+
# the handling of ownership/permissions on specific files
|
2440
|
+
conflicting_files.each do | file, stat |
|
2441
|
+
File.chmod(stat.mode, file)
|
2442
|
+
File.chown(stat.uid, stat.gid, file)
|
2443
|
+
end
|
2444
|
+
|
2445
|
+
# Handle any decryption and ownership/permissions on specific files
|
2446
|
+
metadata[:files][:files].each do |tpkgfile|
|
2447
|
+
tpkg_path = tpkgfile[:path]
|
2448
|
+
working_path = nil
|
2449
|
+
if tpkg_path[0,1] == File::SEPARATOR
|
2450
|
+
working_path = File.join(workdir, 'tpkg', 'root', tpkg_path)
|
2451
|
+
else
|
2452
|
+
working_path = File.join(workdir, 'tpkg', 'reloc', tpkg_path)
|
2453
|
+
end
|
2454
|
+
if !File.exist?(working_path) && !File.symlink?(working_path)
|
2455
|
+
raise "tpkg.xml for #{File.basename(package_file)} references file #{tpkg_path} but that file is not in the package"
|
2456
|
+
end
|
2457
|
+
|
2458
|
+
# Set permissions and ownership for specific files
|
2459
|
+
# We do this before the decryption stage so that permissions and
|
2460
|
+
# ownership designed to protect private file contents are in place
|
2461
|
+
# prior to decryption. The decrypt method preserves the permissions
|
2462
|
+
# and ownership of the encrypted file on the decrypted file.
|
2463
|
+
if tpkgfile[:posix]
|
2464
|
+
if tpkgfile[:posix][:owner] || tpkgfile[:posix][:group]
|
2465
|
+
uid = nil
|
2466
|
+
if tpkgfile[:posix][:owner]
|
2467
|
+
uid = Tpkg::lookup_uid(tpkgfile[:posix][:owner])
|
2468
|
+
end
|
2469
|
+
gid = nil
|
2470
|
+
if tpkgfile[:posix][:group]
|
2471
|
+
gid = Tpkg::lookup_gid(tpkgfile[:posix][:group])
|
2472
|
+
end
|
2473
|
+
begin
|
2474
|
+
File.chown(uid, gid, working_path)
|
2475
|
+
rescue Errno::EPERM
|
2476
|
+
raise if Process.euid == 0
|
2477
|
+
end
|
2478
|
+
end
|
2479
|
+
if tpkgfile[:posix][:perms]
|
2480
|
+
perms = tpkgfile[:posix][:perms]
|
2481
|
+
File.chmod(perms, working_path)
|
2482
|
+
end
|
2483
|
+
end
|
2484
|
+
|
2485
|
+
# Decrypt any files marked for decryption
|
2486
|
+
if tpkgfile[:encrypt]
|
2487
|
+
if passphrase.nil?
|
2488
|
+
# If the user didn't supply a passphrase then just remove the
|
2489
|
+
# encrypted file. This allows users to install packages that
|
2490
|
+
# contain encrypted files for which they don't have the
|
2491
|
+
# passphrase. They end up with just the non-encrypted files,
|
2492
|
+
# potentially useful for development or QA environments.
|
2493
|
+
File.delete(working_path)
|
2494
|
+
else
|
2495
|
+
(1..3).each do | i |
|
2496
|
+
begin
|
2497
|
+
Tpkg::decrypt(metadata[:name], working_path, passphrase)
|
2498
|
+
break
|
2499
|
+
rescue OpenSSL::CipherError
|
2500
|
+
@@passphrase = nil
|
2501
|
+
if i == 3
|
2502
|
+
raise "Incorrect passphrase."
|
2503
|
+
else
|
2504
|
+
puts "Incorrect passphrase. Try again."
|
2505
|
+
end
|
2506
|
+
end
|
2507
|
+
end
|
2508
|
+
|
2509
|
+
#digest = Digest::SHA256.file(working_path).hexdigest
|
2510
|
+
digest = Digest::SHA256.hexdigest(File.read(working_path))
|
2511
|
+
# get checksum for the decrypted file. Will be used for creating file_metadata.xml
|
2512
|
+
checksums_of_decrypted_files[File.expand_path(tpkg_path)] = digest
|
2513
|
+
end
|
2514
|
+
end
|
2515
|
+
end if metadata[:files] && metadata[:files][:files]
|
2516
|
+
|
2517
|
+
# We should get the perms, gid, uid stuff here since all the files
|
2518
|
+
# have been set up correctly
|
2519
|
+
Find.find(root_dir, reloc_dir) do |f|
|
2520
|
+
# If the package doesn't contain either of the top level
|
2521
|
+
# directory we need to skip them, find will pass them to us
|
2522
|
+
# even if they don't exist.
|
2523
|
+
next if !File.exist?(f)
|
2524
|
+
next if File.symlink?(f)
|
2525
|
+
|
2526
|
+
# check if it's from root dir or reloc dir
|
2527
|
+
if f =~ /^#{root_dir}/
|
2528
|
+
short_fn = f[root_dir.length ..-1]
|
2529
|
+
else
|
2530
|
+
short_fn = f[reloc_dir.length + 1..-1]
|
2531
|
+
relocatable = "true"
|
2532
|
+
end
|
2533
|
+
|
2534
|
+
acl = {}
|
2535
|
+
acl["gid"] = File.stat(f).gid
|
2536
|
+
acl["uid"] = File.stat(f).uid
|
2537
|
+
acl["perms"] = File.stat(f).mode.to_s(8)
|
2538
|
+
files_info[short_fn] = acl
|
2539
|
+
end
|
2540
|
+
|
2541
|
+
# Move files into place
|
2542
|
+
# If we implement any of the ACL permissions features we'll have to be
|
2543
|
+
# careful here that tar preserves those permissions. Otherwise we'll
|
2544
|
+
# need to apply them after moving the files into place.
|
2545
|
+
if File.directory?(File.join(workdir, 'tpkg', 'root'))
|
2546
|
+
system("#{@tar} -C #{File.join(workdir, 'tpkg', 'root')} -cf - . | #{@tar} -C #{@file_system_root} -xpf -")
|
2547
|
+
end
|
2548
|
+
if File.directory?(File.join(workdir, 'tpkg', 'reloc'))
|
2549
|
+
system("#{@tar} -C #{File.join(workdir, 'tpkg', 'reloc')} -cf - . | #{@tar} -C #{@base} -xpf -")
|
2550
|
+
end
|
2551
|
+
|
2552
|
+
# Install any init scripts
|
2553
|
+
init_links(metadata).each do |link, init_script|
|
2554
|
+
# We don't have to any anything if there's already symlink to our init script.
|
2555
|
+
# This can happen if user removes pkg manually without removing
|
2556
|
+
# init symlink
|
2557
|
+
next if File.symlink?(link) && File.readlink(link) == init_script
|
2558
|
+
begin
|
2559
|
+
if !File.exist?(File.dirname(link))
|
2560
|
+
FileUtils.mkdir_p(File.dirname(link))
|
2561
|
+
end
|
2562
|
+
begin
|
2563
|
+
File.symlink(init_script, link)
|
2564
|
+
rescue Errno::EEXIST
|
2565
|
+
# The link name that init_links provides is not guaranteed to
|
2566
|
+
# be unique. It might collide with a base system init script
|
2567
|
+
# or an init script from another tpkg. If the link name
|
2568
|
+
# supplied by init_links results in EEXIST then try appending
|
2569
|
+
# a number to the end of the link name.
|
2570
|
+
catch :init_link_done do
|
2571
|
+
1.upto(9) do |i|
|
2572
|
+
begin
|
2573
|
+
File.symlink(init_script, link + i.to_s)
|
2574
|
+
throw :init_link_done
|
2575
|
+
rescue Errno::EEXIST
|
2576
|
+
end
|
2577
|
+
end
|
2578
|
+
# If we get here (i.e. we never reached the throw) then we
|
2579
|
+
# failed to create any of the possible link names.
|
2580
|
+
raise "Failed to install init script #{init_script} -> #{link} for #{File.basename(package_file)}"
|
2581
|
+
end
|
2582
|
+
end
|
2583
|
+
rescue Errno::EPERM
|
2584
|
+
# If creating the link fails due to permission problems and
|
2585
|
+
# we're not running as root just warn the user, allowing folks
|
2586
|
+
# to run tpkg as a non-root user with reduced functionality.
|
2587
|
+
if Process.euid == 0
|
2588
|
+
raise
|
2589
|
+
else
|
2590
|
+
warn "Failed to install init script for #{File.basename(package_file)}, probably due to lack of root privileges"
|
2591
|
+
end
|
2592
|
+
end
|
2593
|
+
end
|
2594
|
+
|
2595
|
+
# Install any crontabs
|
2596
|
+
crontab_destinations(metadata).each do |crontab, destination|
|
2597
|
+
begin
|
2598
|
+
if destination[:link]
|
2599
|
+
next if File.symlink?(destination[:link]) && File.readlink(destination[:link]) == crontab
|
2600
|
+
if !File.exist?(File.dirname(destination[:link]))
|
2601
|
+
FileUtils.mkdir_p(File.dirname(destination[:link]))
|
2602
|
+
end
|
2603
|
+
begin
|
2604
|
+
File.symlink(crontab, destination[:link])
|
2605
|
+
rescue Errno::EEXIST
|
2606
|
+
# The link name that crontab_destinations provides is not
|
2607
|
+
# guaranteed to be unique. It might collide with a base
|
2608
|
+
# system crontab or a crontab from another tpkg. If the
|
2609
|
+
# link name supplied by crontab_destinations results in
|
2610
|
+
# EEXIST then try appending a number to the end of the link
|
2611
|
+
# name.
|
2612
|
+
catch :crontab_link_done do
|
2613
|
+
1.upto(9) do |i|
|
2614
|
+
begin
|
2615
|
+
File.symlink(crontab, destination[:link] + i.to_s)
|
2616
|
+
throw :crontab_link_done
|
2617
|
+
rescue Errno::EEXIST
|
2618
|
+
end
|
2619
|
+
end
|
2620
|
+
# If we get here (i.e. we never reached the throw) then we
|
2621
|
+
# failed to create any of the possible link names.
|
2622
|
+
raise "Failed to install crontab #{crontab} -> #{destination[:link]} for #{File.basename(package_file)}"
|
2623
|
+
end
|
2624
|
+
end
|
2625
|
+
elsif destination[:file]
|
2626
|
+
if !File.exist?(File.dirname(destination[:file]))
|
2627
|
+
FileUtils.mkdir_p(File.dirname(destination[:file]))
|
2628
|
+
end
|
2629
|
+
tmpfile = Tempfile.new(File.basename(destination[:file]), File.dirname(destination[:file]))
|
2630
|
+
if File.exist?(destination[:file])
|
2631
|
+
# Match permissions and ownership of current crontab
|
2632
|
+
st = File.stat(destination[:file])
|
2633
|
+
File.chmod(st.mode & 07777, tmpfile.path)
|
2634
|
+
File.chown(st.uid, st.gid, tmpfile.path)
|
2635
|
+
# Insert the contents of the current crontab file
|
2636
|
+
File.open(destination[:file]) { |file| tmpfile.write(file.read) }
|
2637
|
+
end
|
2638
|
+
# Insert a header line so we can find this section to remove later
|
2639
|
+
tmpfile.puts "### TPKG START - #{@base} - #{File.basename(package_file)}"
|
2640
|
+
# Insert the package crontab contents
|
2641
|
+
crontab_contents = IO.read(crontab)
|
2642
|
+
tmpfile.write(crontab_contents)
|
2643
|
+
# Insert a newline if the crontab doesn't end with one
|
2644
|
+
if crontab_contents.chomp == crontab_contents
|
2645
|
+
tmpfile.puts
|
2646
|
+
end
|
2647
|
+
# Insert a footer line
|
2648
|
+
tmpfile.puts "### TPKG END - #{@base} - #{File.basename(package_file)}"
|
2649
|
+
tmpfile.close
|
2650
|
+
File.rename(tmpfile.path, destination[:file])
|
2651
|
+
# FIXME: On Solaris we should bounce cron or use the crontab
|
2652
|
+
# command, otherwise cron won't pick up the changes
|
2653
|
+
end
|
2654
|
+
rescue Errno::EPERM
|
2655
|
+
# If installing the crontab fails due to permission problems and
|
2656
|
+
# we're not running as root just warn the user, allowing folks
|
2657
|
+
# to run tpkg as a non-root user with reduced functionality.
|
2658
|
+
if Process.euid == 0
|
2659
|
+
raise
|
2660
|
+
else
|
2661
|
+
warn "Failed to install crontab for #{File.basename(package_file)}, probably due to lack of root privileges"
|
2662
|
+
end
|
2663
|
+
end
|
2664
|
+
end
|
2665
|
+
|
2666
|
+
# Run postinstall script
|
2667
|
+
if File.exist?(File.join(workdir, 'tpkg', 'postinstall'))
|
2668
|
+
pwd = Dir.pwd
|
2669
|
+
# chdir into the working directory so that the user can specify a
|
2670
|
+
# relative path to their file/script.
|
2671
|
+
Dir.chdir(File.join(workdir, 'tpkg'))
|
2672
|
+
|
2673
|
+
# Warn the user about non-executable files, as system will just
|
2674
|
+
# silently fail and exit if that's the case.
|
2675
|
+
if !File.executable?(File.join(workdir, 'tpkg', 'postinstall'))
|
2676
|
+
warn "Warning: postinstall script for #{File.basename(package_file)} is not executable, execution will likely fail"
|
2677
|
+
end
|
2678
|
+
# Note this only warns the user if the postinstall fails, it does
|
2679
|
+
# not raise an exception like we do if preinstall fails. Raising
|
2680
|
+
# an exception would leave the package's files installed but the
|
2681
|
+
# package not registered as installed, which does not seem
|
2682
|
+
# desirable. We could remove the package's files and raise an
|
2683
|
+
# exception, but this seems the best approach to me.
|
2684
|
+
system(File.join(workdir, 'tpkg', 'postinstall')) || warn("Warning: postinstall for #{File.basename(package_file)} failed with exit value #{$?.exitstatus}")
|
2685
|
+
ret_val = POSTINSTALL_ERR if $?.exitstatus > 0
|
2686
|
+
|
2687
|
+
# Switch back to our previous directory
|
2688
|
+
Dir.chdir(pwd)
|
2689
|
+
end
|
2690
|
+
|
2691
|
+
# Save metadata for this pkg
|
2692
|
+
package_name = File.basename(package_file, File.extname(package_file))
|
2693
|
+
package_metadata_dir = File.join(@metadata_directory, package_name)
|
2694
|
+
FileUtils.mkdir_p(package_metadata_dir)
|
2695
|
+
metadata_file = File.new(File.join(package_metadata_dir, "tpkg.yml"), "w")
|
2696
|
+
metadata.write(metadata_file)
|
2697
|
+
metadata_file.close
|
2698
|
+
|
2699
|
+
# Save file_metadata.yml for this pkg
|
2700
|
+
if File.exist?(File.join(workdir, 'tpkg', 'file_metadata.bin'))
|
2701
|
+
file_metadata = FileMetadata.new(File.read(File.join(workdir, 'tpkg', 'file_metadata.bin')), 'bin')
|
2702
|
+
elsif File.exist?(File.join(workdir, 'tpkg', 'file_metadata.yml'))
|
2703
|
+
file_metadata = FileMetadata.new(File.read(File.join(workdir, 'tpkg', 'file_metadata.yml')), 'yml')
|
2704
|
+
elsif File.exists?(File.join(workdir, 'tpkg', 'file_metadata.xml'))
|
2705
|
+
file_metadata = FileMetadata.new(File.read(File.join(workdir, 'tpkg', 'file_metadata.xml')), 'xml')
|
2706
|
+
end
|
2707
|
+
if file_metadata
|
2708
|
+
file_metadata[:package_file] = File.basename(package_file)
|
2709
|
+
file_metadata[:files].each do |file|
|
2710
|
+
acl = files_info[file[:path]]
|
2711
|
+
file.merge!(acl) unless acl.nil?
|
2712
|
+
digest = checksums_of_decrypted_files[File.expand_path(file[:path])]
|
2713
|
+
if digest
|
2714
|
+
digests = file[:checksum][:digests]
|
2715
|
+
digests[0][:encrypted] = true
|
2716
|
+
digests[1] = {:decrypted => true, :value => digest}
|
2717
|
+
end
|
2718
|
+
end
|
2719
|
+
|
2720
|
+
file = File.open(File.join(package_metadata_dir, "file_metadata.bin"), "w")
|
2721
|
+
Marshal.dump(file_metadata.hash, file)
|
2722
|
+
file.close
|
2723
|
+
else
|
2724
|
+
warn "Warning: package #{File.basename(package_file)} does not include file_metadata information."
|
2725
|
+
end
|
2726
|
+
|
2727
|
+
# Copy the package file to the directory for installed packages
|
2728
|
+
FileUtils.cp(package_file, @installed_directory)
|
2729
|
+
|
2730
|
+
# Cleanup
|
2731
|
+
FileUtils.rm_rf(workdir)
|
2732
|
+
return ret_val
|
2733
|
+
end
|
2734
|
+
|
2735
|
+
def requirements_for_currently_installed_package(pkgname=nil)
|
2736
|
+
requirements = []
|
2737
|
+
metadata_for_installed_packages.each do |metadata|
|
2738
|
+
if !pkgname || pkgname == metadata[:name]
|
2739
|
+
req = { :name => metadata[:name],
|
2740
|
+
:minimum_version => metadata[:version] }
|
2741
|
+
if metadata[:package_version]
|
2742
|
+
req[:minimum_package_version] = metadata[:package_version]
|
2743
|
+
end
|
2744
|
+
requirements << req
|
2745
|
+
end
|
2746
|
+
end
|
2747
|
+
requirements
|
2748
|
+
end
|
2749
|
+
|
2750
|
+
# Adds/modifies requirements and packages arguments to add requirements
|
2751
|
+
# and package entries for currently installed packages
|
2752
|
+
# Note: the requirements and packages arguments are modified by this method
|
2753
|
+
def requirements_for_currently_installed_packages(requirements, packages)
|
2754
|
+
metadata_for_installed_packages.each do |installed_xml|
|
2755
|
+
name = installed_xml[:name]
|
2756
|
+
version = installed_xml[:version]
|
2757
|
+
# For each currently installed package we insert a requirement for
|
2758
|
+
# at least that version of the package
|
2759
|
+
req = { :name => name, :minimum_version => version }
|
2760
|
+
requirements << req
|
2761
|
+
# Initialize the list of possible packages for this req
|
2762
|
+
if !packages[name]
|
2763
|
+
packages[name] = available_packages_that_meet_requirement(req)
|
2764
|
+
end
|
2765
|
+
end
|
2766
|
+
end
|
2767
|
+
|
2768
|
+
# Define requirements for requested packages
|
2769
|
+
# Takes an array of packages: files, URLs, or basic package specs ('foo' or
|
2770
|
+
# 'foo=1.0')
|
2771
|
+
# Adds/modifies requirements and packages arguments based on parsing those
|
2772
|
+
# requests
|
2773
|
+
# Input:
|
2774
|
+
# [ 'foo-1.0.tpkg', 'http://server/pkgs/bar-2.3.pkg', 'blat=0.5' ]
|
2775
|
+
# Result:
|
2776
|
+
# requirements << { :name => 'foo' }, packages['foo'] = { :source => 'foo-1.0.tpkg' }
|
2777
|
+
# requirements << { :name => 'bar' }, packages['bar'] = { :source => 'http://server/pkgs/bar-2.3.pkg' }
|
2778
|
+
# requirements << { :name => 'blat', :minimum_version => '0.5', :maximum_version => '0.5' }, packages['blat'] populated with available packages meeting that requirement
|
2779
|
+
# Note: the requirements and packages arguments are modified by this method
|
2780
|
+
def parse_requests(requests, requirements, packages)
|
2781
|
+
newreqs = []
|
2782
|
+
|
2783
|
+
requests.each do |request|
|
2784
|
+
puts "parse_requests processing #{request.inspect}" if @@debug
|
2785
|
+
if request =~ /^[-\w=<>\d\.]+$/ && !File.file?(request) # basic package specs ('foo' or 'foo=1.0')
|
2786
|
+
puts "parse_requests request looks like package spec" if @@debug
|
2787
|
+
|
2788
|
+
# Tpkg::parse_request is a class method and doesn't know where packages are installed.
|
2789
|
+
# So we have to tell it ourselves.
|
2790
|
+
req = Tpkg::parse_request(request, @installed_directory)
|
2791
|
+
newreqs << req
|
2792
|
+
|
2793
|
+
# Initialize the list of possible packages for this req
|
2794
|
+
if !packages[req[:name]]
|
2795
|
+
packages[req[:name]] = available_packages_that_meet_requirement(req)
|
2796
|
+
end
|
2797
|
+
else # User specified a file or URI
|
2798
|
+
req = {}
|
2799
|
+
metadata = nil
|
2800
|
+
source = nil
|
2801
|
+
localpath = nil
|
2802
|
+
if File.file?(request)
|
2803
|
+
puts "parse_requests treating request as a file" if @@debug
|
2804
|
+
localpath = request
|
2805
|
+
metadata = Tpkg::metadata_from_package(request)
|
2806
|
+
source = request
|
2807
|
+
else
|
2808
|
+
puts "parse_requests treating request as a URI" if @@debug
|
2809
|
+
uri = URI.parse(request) # This just serves as a sanity check
|
2810
|
+
# Using these File methods on a URI seems to work but is probably fragile
|
2811
|
+
source = File.dirname(request) + '/' # dirname chops off the / at the end, we need it in order to be compatible with URI.join
|
2812
|
+
pkgfile = File.basename(request)
|
2813
|
+
localpath = download(source, pkgfile, Tpkg::tempdir('download'))
|
2814
|
+
metadata = Tpkg::metadata_from_package(localpath)
|
2815
|
+
# Cleanup temp download dir
|
2816
|
+
FileUtils.rm_rf(localpath)
|
2817
|
+
end
|
2818
|
+
req[:name] = metadata[:name]
|
2819
|
+
pkg = { :metadata => metadata, :source => source }
|
2820
|
+
|
2821
|
+
newreqs << req
|
2822
|
+
# The user specified a particular package, so it is the only package
|
2823
|
+
# that can be used to meet the requirement
|
2824
|
+
packages[req[:name]] = [pkg]
|
2825
|
+
end
|
2826
|
+
end
|
2827
|
+
|
2828
|
+
requirements.concat(newreqs)
|
2829
|
+
newreqs
|
2830
|
+
end
|
2831
|
+
|
2832
|
+
# After calling parse_request, we should call this method
|
2833
|
+
# to check whether or not we can meet the requirements/dependencies
|
2834
|
+
# of the result packages
|
2835
|
+
def check_requests(packages)
|
2836
|
+
all_requests_satisfied = true # whether or not all requests can be satisfied
|
2837
|
+
errors = [""]
|
2838
|
+
packages.each do |name, pkgs|
|
2839
|
+
if pkgs.empty?
|
2840
|
+
errors << ["Unable to find any packages which satisfy #{name}"]
|
2841
|
+
all_requests_satisfied = false
|
2842
|
+
next
|
2843
|
+
end
|
2844
|
+
|
2845
|
+
request_satisfied = false # whether or not this request can be satisfied
|
2846
|
+
possible_errors = []
|
2847
|
+
pkgs.each do |pkg|
|
2848
|
+
metadata = pkg[:metadata]
|
2849
|
+
req = { :name => metadata[:name] }
|
2850
|
+
# Quick sanity check that the package can be installed on this machine.
|
2851
|
+
if !Tpkg::package_meets_requirement?(pkg, req)
|
2852
|
+
possible_errors << " Requested package #{metadata[:filename]} doesn't match this machine's OS or architecture"
|
2853
|
+
next
|
2854
|
+
end
|
2855
|
+
# a sanity check that there is at least one package
|
2856
|
+
# available for each dependency of this package
|
2857
|
+
dep_satisfied = true
|
2858
|
+
metadata[:dependencies].each do |depreq|
|
2859
|
+
if available_packages_that_meet_requirement(depreq).empty? && !Tpkg::packages_meet_requirement?(packages.values.flatten, depreq)
|
2860
|
+
possible_errors << " Requested package #{metadata[:filename]} depends on #{depreq.inspect}, no packages that satisfy that dependency are available"
|
2861
|
+
dep_satisfied = false
|
2862
|
+
end
|
2863
|
+
end if metadata[:dependencies]
|
2864
|
+
request_satisfied = true if dep_satisfied
|
2865
|
+
end
|
2866
|
+
if !request_satisfied
|
2867
|
+
errors << ["Unable to find any packages which satisfy #{name}. Possible error(s):"]
|
2868
|
+
errors << possible_errors
|
2869
|
+
all_requests_satisfied = false
|
2870
|
+
end
|
2871
|
+
end
|
2872
|
+
|
2873
|
+
if !all_requests_satisfied
|
2874
|
+
puts errors.join("\n")
|
2875
|
+
raise "Unable to satisfy the request(s)"
|
2876
|
+
end
|
2877
|
+
end
|
2878
|
+
|
2879
|
+
CHECK_INSTALL = 1
|
2880
|
+
CHECK_UPGRADE = 2
|
2881
|
+
CHECK_REMOVE = 3
|
2882
|
+
def conflicting_files(package_file, mode=CHECK_INSTALL)
|
2883
|
+
metadata = Tpkg::metadata_from_package(package_file)
|
2884
|
+
pkgname = metadata[:name]
|
2885
|
+
|
2886
|
+
conflicts = {}
|
2887
|
+
|
2888
|
+
installed_files = files_for_installed_packages
|
2889
|
+
|
2890
|
+
# Pull out the normalized paths, skipping appropriate packages based
|
2891
|
+
# on the requested mode
|
2892
|
+
installed_files_normalized = {}
|
2893
|
+
installed_files.each do |pkgfile, files|
|
2894
|
+
# Skip packages with the same name if the user is performing an upgrade
|
2895
|
+
if mode == CHECK_UPGRADE && files[:metadata][:name] == pkgname
|
2896
|
+
next
|
2897
|
+
end
|
2898
|
+
# Skip packages with the same filename if the user is removing
|
2899
|
+
if mode == CHECK_REMOVE && pkgfile == File.basename(package_file)
|
2900
|
+
next
|
2901
|
+
end
|
2902
|
+
installed_files_normalized[pkgfile] = files[:normalized]
|
2903
|
+
end
|
2904
|
+
|
2905
|
+
fip = Tpkg::files_in_package(package_file)
|
2906
|
+
normalize_paths(fip)
|
2907
|
+
|
2908
|
+
fip[:normalized].each do |file|
|
2909
|
+
installed_files_normalized.each do |instpkgfile, files|
|
2910
|
+
if files.include?(file)
|
2911
|
+
if !conflicts[instpkgfile]
|
2912
|
+
conflicts[instpkgfile] = []
|
2913
|
+
end
|
2914
|
+
conflicts[instpkgfile] << file
|
2915
|
+
end
|
2916
|
+
end
|
2917
|
+
end
|
2918
|
+
|
2919
|
+
# The remove method actually needs !conflicts, so invert in that case
|
2920
|
+
if mode == CHECK_REMOVE
|
2921
|
+
# Flatten conflicts to an array
|
2922
|
+
flatconflicts = []
|
2923
|
+
conflicts.each_value { |files| flatconflicts.concat(files) }
|
2924
|
+
# And invert
|
2925
|
+
conflicts = fip[:normalized] - flatconflicts
|
2926
|
+
end
|
2927
|
+
|
2928
|
+
conflicts
|
2929
|
+
end
|
2930
|
+
|
2931
|
+
def check_for_conflicting_pkgs(pkgs_to_check)
|
2932
|
+
# loop through packages that we're interested in, check for conflict listing,
|
2933
|
+
# see if there are any conflicts among each other
|
2934
|
+
pkgs_to_check.each do |pkg1|
|
2935
|
+
# native package might not have conflicts defined so skip
|
2936
|
+
next if pkg1[:metadata][:conflicts].nil?
|
2937
|
+
pkg1[:metadata][:conflicts].each do | conflict |
|
2938
|
+
pkgs_to_check.each do |pkg2|
|
2939
|
+
if Tpkg::package_meets_requirement?(pkg2, conflict)
|
2940
|
+
raise "Package conflicts between #{pkg2.inspect} and #{pkg1.inspect}"
|
2941
|
+
end
|
2942
|
+
end
|
2943
|
+
end
|
2944
|
+
end
|
2945
|
+
end
|
2946
|
+
|
2947
|
+
def prompt_for_conflicting_files(package_file, mode=CHECK_INSTALL)
|
2948
|
+
if !@@prompt
|
2949
|
+
return true
|
2950
|
+
end
|
2951
|
+
|
2952
|
+
result = true
|
2953
|
+
conflicts = conflicting_files(package_file, mode)
|
2954
|
+
|
2955
|
+
# We don't want to prompt the user for directories, so strip those out
|
2956
|
+
conflicts.each do |pkgfile, files|
|
2957
|
+
files.reject! { |file| File.directory?(file) }
|
2958
|
+
end
|
2959
|
+
conflicts.reject! { |pkgfile, files| files.empty? }
|
2960
|
+
|
2961
|
+
if !conflicts.empty?
|
2962
|
+
puts "File conflicts:"
|
2963
|
+
conflicts.each do |pkgfile, files|
|
2964
|
+
files.each do |file|
|
2965
|
+
puts "#{file} (#{pkgfile})"
|
2966
|
+
end
|
2967
|
+
end
|
2968
|
+
print "Proceed? [y/N] "
|
2969
|
+
response = $stdin.gets
|
2970
|
+
if response !~ /^y/i
|
2971
|
+
result = false
|
2972
|
+
end
|
2973
|
+
end
|
2974
|
+
result
|
2975
|
+
end
|
2976
|
+
|
2977
|
+
def prompt_for_install(pkgs, promptstring)
|
2978
|
+
if @@prompt
|
2979
|
+
pkgs_to_report = pkgs.select do |pkg|
|
2980
|
+
pkg[:source] != :currently_installed &&
|
2981
|
+
pkg[:source] != :native_installed
|
2982
|
+
end
|
2983
|
+
if !pkgs_to_report.empty?
|
2984
|
+
puts "The following packages will be #{promptstring}:"
|
2985
|
+
pkgs_to_report.sort(&SORT_PACKAGES).each do |pkg|
|
2986
|
+
if pkg[:source] == :native_available
|
2987
|
+
name = pkg[:metadata][:name]
|
2988
|
+
version = pkg[:metadata][:version]
|
2989
|
+
package_version = pkg[:metadata][:package_version]
|
2990
|
+
puts "Native #{name}=#{version}=#{package_version}"
|
2991
|
+
else
|
2992
|
+
puts pkg[:metadata][:filename]
|
2993
|
+
end
|
2994
|
+
end
|
2995
|
+
return Tpkg::confirm
|
2996
|
+
end
|
2997
|
+
end
|
2998
|
+
true
|
2999
|
+
end
|
3000
|
+
|
3001
|
+
# See parse_requests for format of requests
|
3002
|
+
def install(requests, passphrase=nil)
|
3003
|
+
ret_val = 0
|
3004
|
+
requirements = []
|
3005
|
+
packages = {}
|
3006
|
+
lock
|
3007
|
+
|
3008
|
+
parse_requests(requests, requirements, packages)
|
3009
|
+
check_requests(packages)
|
3010
|
+
core_packages = []
|
3011
|
+
currently_installed_requirements = []
|
3012
|
+
requirements.each do |req|
|
3013
|
+
core_packages << req[:name] if !core_packages.include?(req[:name])
|
3014
|
+
currently_installed_requirements.concat(
|
3015
|
+
requirements_for_currently_installed_package(req[:name]))
|
3016
|
+
end
|
3017
|
+
requirements.concat(currently_installed_requirements).uniq!
|
3018
|
+
|
3019
|
+
puts "install calling best_solution" if @@debug
|
3020
|
+
puts "install requirements: #{requirements.inspect}" if @@debug
|
3021
|
+
puts "install packages: #{packages.inspect}" if @@debug
|
3022
|
+
puts "install core_packages: #{core_packages.inspect}" if @@debug
|
3023
|
+
#solution_packages = best_solution(requirements.dup, packages.dup)
|
3024
|
+
solution_packages = best_solution(requirements, packages, core_packages)
|
3025
|
+
if !solution_packages
|
3026
|
+
raise "Unable to resolve dependencies"
|
3027
|
+
end
|
3028
|
+
|
3029
|
+
check_for_conflicting_pkgs(solution_packages | installed_packages)
|
3030
|
+
|
3031
|
+
if !prompt_for_install(solution_packages, 'installed')
|
3032
|
+
unlock
|
3033
|
+
return false
|
3034
|
+
end
|
3035
|
+
|
3036
|
+
# Create array of packages (names) we have installed so far
|
3037
|
+
# We will use it later on to determine the order of how to install the packages
|
3038
|
+
installed_so_far = installed_packages.collect{|pkg| pkg[:metadata][:name]}
|
3039
|
+
|
3040
|
+
while pkg = solution_packages.shift
|
3041
|
+
# get dependencies and make sure we install the packages in the correct order
|
3042
|
+
# based on the dependencies
|
3043
|
+
dependencies = nil
|
3044
|
+
if pkg[:metadata][:dependencies]
|
3045
|
+
dependencies = pkg[:metadata][:dependencies].collect { |dep| dep[:name] }.compact
|
3046
|
+
# don't install this pkg right now if its dependencies haven't been installed
|
3047
|
+
if !dependencies.empty? && !dependencies.to_set.subset?(installed_so_far.to_set)
|
3048
|
+
solution_packages.push(pkg)
|
3049
|
+
next
|
3050
|
+
end
|
3051
|
+
end
|
3052
|
+
|
3053
|
+
if pkg[:source] == :currently_installed ||
|
3054
|
+
pkg[:source] == :native_installed
|
3055
|
+
# Nothing to do for packages currently installed
|
3056
|
+
warn "Skipping #{pkg[:metadata][:name]}, already installed"
|
3057
|
+
elsif pkg[:source] == :native_available
|
3058
|
+
if Tpkg::get_os =~ /RedHat|CentOS|Fedora/
|
3059
|
+
name = pkg[:metadata][:name]
|
3060
|
+
version = pkg[:metadata][:version]
|
3061
|
+
package_version = pkg[:metadata][:package_version]
|
3062
|
+
# RPMs always have a release/package_version
|
3063
|
+
pkgname = "#{name}-#{version}-#{package_version}"
|
3064
|
+
puts "Running 'yum -y install #{pkgname}' to install native package" if @@debug
|
3065
|
+
system("yum -y install #{pkgname}")
|
3066
|
+
elsif Tpkg::get_os =~ /Debian|Ubuntu/
|
3067
|
+
name = pkg[:metadata][:name]
|
3068
|
+
version = pkg[:metadata][:version]
|
3069
|
+
pkgname = "#{name}-#{version}"
|
3070
|
+
if pkg[:metadata][:package_version]
|
3071
|
+
pkgname << "-#{pkg[:metadata][:package_version]}"
|
3072
|
+
end
|
3073
|
+
puts "Running 'apt-get -y install #{pkgname}' to install native package" if @@debug
|
3074
|
+
system("apt-get -y install #{pkgname}")
|
3075
|
+
elsif Tpkg::get_os =~ /Solaris/
|
3076
|
+
name = pkg[:metadata][:name]
|
3077
|
+
version = pkg[:metadata][:version]
|
3078
|
+
pkgname = "#{name}-#{version}"
|
3079
|
+
if pkg[:metadata][:package_version]
|
3080
|
+
pkgname << ",REV=#{pkg[:metadata][:package_version]}"
|
3081
|
+
end
|
3082
|
+
if File.exist?('/opt/csw/bin/pkg-get')
|
3083
|
+
puts "Running '/opt/csw/bin/pkg-get -i #{pkgname}' to install native package" if @@debug
|
3084
|
+
system("/opt/csw/bin/pkg-get -i #{pkgname}")
|
3085
|
+
else
|
3086
|
+
raise "No native package installation tool available"
|
3087
|
+
end
|
3088
|
+
elsif Tpkg::get_os =~ /FreeBSD/
|
3089
|
+
name = pkg[:metadata][:name]
|
3090
|
+
version = pkg[:metadata][:version]
|
3091
|
+
pkgname = "#{name}-#{version}"
|
3092
|
+
if pkg[:metadata][:package_version]
|
3093
|
+
pkgname << "_#{pkg[:metadata][:package_version]}"
|
3094
|
+
end
|
3095
|
+
puts "Running 'pkg_add -r #{pkgname}' to install native package" if @@debug
|
3096
|
+
system("pkg_add -r #{pkgname}")
|
3097
|
+
elsif Tpkg::get_os =~ /Darwin/
|
3098
|
+
if File.exist?('/opt/local/bin/port')
|
3099
|
+
name = pkg[:metadata][:name]
|
3100
|
+
# MacPorts doesn't support installing a specific version (AFAIK)
|
3101
|
+
if pkg[:metadata][:version]
|
3102
|
+
warn "Ignoring version with MacPorts"
|
3103
|
+
end
|
3104
|
+
# Nor does it have a concept of a package version
|
3105
|
+
if pkg[:metadata][:package_version]
|
3106
|
+
warn "Ignoring package version with MacPorts"
|
3107
|
+
end
|
3108
|
+
# Just for consistency with the code for other platforms
|
3109
|
+
pkgname = name
|
3110
|
+
puts "Running '/opt/local/bin/port install #{pkgname}' to install native package" if @@debug
|
3111
|
+
system("/opt/local/bin/port install #{pkgname}")
|
3112
|
+
else
|
3113
|
+
# Fink support would be nice
|
3114
|
+
raise "No supported native package tool available on #{Tpkg::get_os}"
|
3115
|
+
end
|
3116
|
+
else
|
3117
|
+
raise "No native package installation support for #{Tpkg::get_os}"
|
3118
|
+
end
|
3119
|
+
else # regular tpkg that needs to be installed
|
3120
|
+
pkgfile = nil
|
3121
|
+
if File.file?(pkg[:source])
|
3122
|
+
pkgfile = pkg[:source]
|
3123
|
+
elsif File.directory?(pkg[:source])
|
3124
|
+
pkgfile = File.join(pkg[:source], pkg[:metadata][:filename])
|
3125
|
+
else
|
3126
|
+
pkgfile = download(pkg[:source], pkg[:metadata][:filename])
|
3127
|
+
end
|
3128
|
+
if File.exist?(
|
3129
|
+
File.join(@installed_directory, File.basename(pkgfile)))
|
3130
|
+
warn "Skipping #{File.basename(pkgfile)}, already installed"
|
3131
|
+
else
|
3132
|
+
if prompt_for_conflicting_files(pkgfile)
|
3133
|
+
ret_val |= unpack(pkgfile, passphrase)
|
3134
|
+
end
|
3135
|
+
end
|
3136
|
+
end
|
3137
|
+
|
3138
|
+
# If we're down here, it means we have installed the package. So go ahead and
|
3139
|
+
# update the list of packages we installed so far
|
3140
|
+
installed_so_far << pkg[:metadata][:name]
|
3141
|
+
end # end while loop
|
3142
|
+
|
3143
|
+
send_update_to_server unless @report_server.nil?
|
3144
|
+
unlock
|
3145
|
+
return ret_val
|
3146
|
+
end
|
3147
|
+
|
3148
|
+
# This method can also be used for doing downgrade
|
3149
|
+
def upgrade(requests=nil, passphrase=nil, downgrade=false)
|
3150
|
+
ret_val = 0
|
3151
|
+
requirements = []
|
3152
|
+
packages = {}
|
3153
|
+
core_packages = []
|
3154
|
+
lock
|
3155
|
+
has_updates = false # flags whether or not there was at least one actual package that
|
3156
|
+
# get updated
|
3157
|
+
|
3158
|
+
# If the user specified some specific packages to upgrade in requests
|
3159
|
+
# then we look for upgrades for just those packages (and any necessary
|
3160
|
+
# dependency upgrades). If the user did not specify specific packages
|
3161
|
+
# then we look for upgrades for all currently installed packages.
|
3162
|
+
|
3163
|
+
if requests
|
3164
|
+
puts "Upgrading requested packages only" if @@debug
|
3165
|
+
parse_requests(requests, requirements, packages)
|
3166
|
+
check_requests(packages)
|
3167
|
+
additional_requirements = []
|
3168
|
+
requirements.each do |req|
|
3169
|
+
core_packages << req[:name] if !core_packages.include?(req[:name])
|
3170
|
+
|
3171
|
+
# When doing downgrade, we don't want to include the package being
|
3172
|
+
# downgrade as the requirements. Otherwise, we won't be able to downgrade it
|
3173
|
+
unless downgrade
|
3174
|
+
additional_requirements.concat(
|
3175
|
+
requirements_for_currently_installed_package(req[:name]))
|
3176
|
+
end
|
3177
|
+
|
3178
|
+
# Initialize the list of possible packages for this req
|
3179
|
+
if !packages[req[:name]]
|
3180
|
+
packages[req[:name]] = available_packages_that_meet_requirement(req)
|
3181
|
+
end
|
3182
|
+
# Remove preference for currently installed package
|
3183
|
+
packages[req[:name]].each do |pkg|
|
3184
|
+
if pkg[:source] == :currently_installed
|
3185
|
+
pkg[:prefer] = false
|
3186
|
+
end
|
3187
|
+
end
|
3188
|
+
|
3189
|
+
# Look for pkgs that might depend on the pkg we're upgrading,
|
3190
|
+
# and add them to our list of requirements. We need to make sure that we can still
|
3191
|
+
# satisfy the dependency requirements if we were to do the upgrade.
|
3192
|
+
metadata_for_installed_packages.each do | metadata |
|
3193
|
+
metadata[:dependencies].each do | dep |
|
3194
|
+
if dep[:name] == req[:name]
|
3195
|
+
additional_requirements << metadata.hash
|
3196
|
+
end
|
3197
|
+
end if metadata[:dependencies]
|
3198
|
+
end
|
3199
|
+
end
|
3200
|
+
requirements.concat(additional_requirements)
|
3201
|
+
requirements.uniq!
|
3202
|
+
else
|
3203
|
+
puts "Upgrading all packages" if @@debug
|
3204
|
+
requirements_for_currently_installed_packages(requirements, packages)
|
3205
|
+
# Remove preference for currently installed packages
|
3206
|
+
packages.each do |name, pkgs|
|
3207
|
+
core_packages << name if !core_packages.include?(name)
|
3208
|
+
pkgs.each do |pkg|
|
3209
|
+
if pkg[:source] == :currently_installed
|
3210
|
+
pkg[:prefer] = false
|
3211
|
+
end
|
3212
|
+
end
|
3213
|
+
end
|
3214
|
+
end
|
3215
|
+
|
3216
|
+
puts "upgrade calling best_solution" if @@debug
|
3217
|
+
puts "upgrade requirements: #{requirements.inspect}" if @@debug
|
3218
|
+
puts "upgrade packages: #{packages.inspect}" if @@debug
|
3219
|
+
puts "upgrade core_packages: #{core_packages.inspect}" if @@debug
|
3220
|
+
#solution_packages = best_solution(requirements.dup, packages.dup)
|
3221
|
+
solution_packages = best_solution(requirements, packages, core_packages)
|
3222
|
+
|
3223
|
+
if solution_packages.nil?
|
3224
|
+
raise "Unable to find solution for upgrading. Please verify that you specified the correct package(s) for upgrade."
|
3225
|
+
end
|
3226
|
+
|
3227
|
+
check_for_conflicting_pkgs(solution_packages | installed_packages)
|
3228
|
+
|
3229
|
+
if downgrade
|
3230
|
+
prompt_action = 'downgraded'
|
3231
|
+
else
|
3232
|
+
prompt_action = 'upgraded'
|
3233
|
+
end
|
3234
|
+
if !prompt_for_install(solution_packages, prompt_action)
|
3235
|
+
unlock
|
3236
|
+
return false
|
3237
|
+
end
|
3238
|
+
|
3239
|
+
installed_files = files_for_installed_packages
|
3240
|
+
removed_pkgs = [] # keep track of what we removed so far
|
3241
|
+
while pkg = solution_packages.shift
|
3242
|
+
# solution_packages.each do |pkg|
|
3243
|
+
if pkg[:source] == :currently_installed ||
|
3244
|
+
pkg[:source] == :native_installed
|
3245
|
+
# Nothing to do for packages currently installed
|
3246
|
+
elsif pkg[:source] == :native_available
|
3247
|
+
if Tpkg::get_os =~ /RedHat|CentOS|Fedora/
|
3248
|
+
name = pkg[:metadata][:name]
|
3249
|
+
version = pkg[:metadata][:version]
|
3250
|
+
package_version = pkg[:metadata][:package_version]
|
3251
|
+
# RPMs always have a release/package_version
|
3252
|
+
pkgname = "#{name}-#{version}-#{package_version}"
|
3253
|
+
puts "Running 'yum -y install #{pkgname}' to upgrade native package" if @@debug
|
3254
|
+
system("yum -y install #{pkgname}")
|
3255
|
+
has_updates = true
|
3256
|
+
elsif Tpkg::get_os =~ /Debian|Ubuntu/
|
3257
|
+
name = pkg[:metadata][:name]
|
3258
|
+
version = pkg[:metadata][:version]
|
3259
|
+
pkgname = "#{name}-#{version}"
|
3260
|
+
if pkg[:metadata][:package_version]
|
3261
|
+
pkgname << "-#{pkg[:metadata][:package_version]}"
|
3262
|
+
end
|
3263
|
+
puts "Running 'apt-get -y install #{pkgname}' to upgrade native package" if @@debug
|
3264
|
+
system("apt-get -y install #{pkgname}")
|
3265
|
+
has_updates = true
|
3266
|
+
elsif Tpkg::get_os =~ /Solaris/
|
3267
|
+
name = pkg[:metadata][:name]
|
3268
|
+
version = pkg[:metadata][:version]
|
3269
|
+
pkgname = "#{name}-#{version}"
|
3270
|
+
if pkg[:metadata][:package_version]
|
3271
|
+
pkgname << ",REV=#{pkg[:metadata][:package_version]}"
|
3272
|
+
end
|
3273
|
+
if File.exist?('/opt/csw/bin/pkg-get')
|
3274
|
+
puts "Running '/opt/csw/bin/pkg-get -i #{pkgname}' to upgrade native package" if @@debug
|
3275
|
+
system("/opt/csw/bin/pkg-get -i #{pkgname}")
|
3276
|
+
has_updates = true
|
3277
|
+
else
|
3278
|
+
raise "No native package upgrade tool available"
|
3279
|
+
end
|
3280
|
+
elsif Tpkg::get_os =~ /FreeBSD/
|
3281
|
+
name = pkg[:metadata][:name]
|
3282
|
+
version = pkg[:metadata][:version]
|
3283
|
+
pkgname = "#{name}-#{version}"
|
3284
|
+
if pkg[:metadata][:package_version]
|
3285
|
+
pkgname << "_#{pkg[:metadata][:package_version]}"
|
3286
|
+
end
|
3287
|
+
# This is not very ideal. It would be better to download the
|
3288
|
+
# new package, and if the download is successful remove the
|
3289
|
+
# old package and install the new one. The way we're doing it
|
3290
|
+
# here we risk leaving the system with neither version
|
3291
|
+
# installed if the download of the new package fails.
|
3292
|
+
# However, the FreeBSD package tools don't make it easy to
|
3293
|
+
# handle things properly.
|
3294
|
+
puts "Running 'pkg_delete #{name}' and 'pkg_add -r #{pkgname}' to upgrade native package" if @@debug
|
3295
|
+
system("pkg_delete #{name}")
|
3296
|
+
system("pkg_add -r #{pkgname}")
|
3297
|
+
has_updates = true
|
3298
|
+
elsif Tpkg::get_os =~ /Darwin/
|
3299
|
+
if File.exist?('/opt/local/bin/port')
|
3300
|
+
name = pkg[:metadata][:name]
|
3301
|
+
# MacPorts doesn't support installing a specific version (AFAIK)
|
3302
|
+
if pkg[:metadata][:version]
|
3303
|
+
warn "Ignoring version with MacPorts"
|
3304
|
+
end
|
3305
|
+
# Nor does it have a concept of a package version
|
3306
|
+
if pkg[:metadata][:package_version]
|
3307
|
+
warn "Ignoring package version with MacPorts"
|
3308
|
+
end
|
3309
|
+
# Just for consistency with the code for other platforms
|
3310
|
+
pkgname = name
|
3311
|
+
puts "Running '/opt/local/bin/port upgrade #{pkgname}' to upgrade native package" if @@debug
|
3312
|
+
system("/opt/local/bin/port upgrade #{pkgname}")
|
3313
|
+
else
|
3314
|
+
# Fink support would be nice
|
3315
|
+
raise "No supported native package tool available on #{Tpkg::get_os}"
|
3316
|
+
end
|
3317
|
+
else
|
3318
|
+
raise "No native package upgrade support for #{Tpkg::get_os}"
|
3319
|
+
end
|
3320
|
+
else # tpkg
|
3321
|
+
pkgfile = nil
|
3322
|
+
if File.file?(pkg[:source])
|
3323
|
+
pkgfile = pkg[:source]
|
3324
|
+
elsif File.directory?(pkg[:source])
|
3325
|
+
pkgfile = File.join(pkg[:source], pkg[:metadata][:filename])
|
3326
|
+
else
|
3327
|
+
pkgfile = download(pkg[:source], pkg[:metadata][:filename])
|
3328
|
+
end
|
3329
|
+
if prompt_for_conflicting_files(pkgfile, CHECK_UPGRADE)
|
3330
|
+
# If the old and new packages have overlapping externals flag them
|
3331
|
+
# to be skipped so that the external isn't removed and then
|
3332
|
+
# immediately re-added
|
3333
|
+
oldpkgs = installed_packages_that_meet_requirement({:name => pkg[:metadata][:name]})
|
3334
|
+
externals_to_skip = []
|
3335
|
+
pkg[:metadata][:externals].each do |external|
|
3336
|
+
if oldpkgs.all? {|oldpkg| oldpkg[:metadata][:externals].include?(external)}
|
3337
|
+
externals_to_skip << external
|
3338
|
+
end
|
3339
|
+
end if pkg[:metadata][:externals]
|
3340
|
+
|
3341
|
+
# Remove the old package if we haven't done so
|
3342
|
+
unless removed_pkgs.include?(pkg[:metadata][:name])
|
3343
|
+
remove([pkg[:metadata][:name]], :upgrade => true, :externals_to_skip => externals_to_skip)
|
3344
|
+
removed_pkgs << pkg[:metadata][:name]
|
3345
|
+
end
|
3346
|
+
|
3347
|
+
# determine if we can unpack the new version package now by
|
3348
|
+
# looking to see if all of its dependencies have been installed
|
3349
|
+
can_unpack = true
|
3350
|
+
pkg[:metadata][:dependencies].each do | dep |
|
3351
|
+
iptmr = installed_packages_that_meet_requirement(dep)
|
3352
|
+
if iptmr.nil? || iptmr.empty?
|
3353
|
+
can_unpack = false
|
3354
|
+
# Can't unpack yet. so push it back in the solution_packages queue
|
3355
|
+
solution_packages.push(pkg)
|
3356
|
+
break
|
3357
|
+
end
|
3358
|
+
end if pkg[:metadata][:dependencies]
|
3359
|
+
if can_unpack
|
3360
|
+
ret_val |= unpack(pkgfile, passphrase, :externals_to_skip => externals_to_skip)
|
3361
|
+
end
|
3362
|
+
|
3363
|
+
has_updates = true
|
3364
|
+
end
|
3365
|
+
end
|
3366
|
+
end
|
3367
|
+
|
3368
|
+
if !has_updates
|
3369
|
+
puts "No updates available"
|
3370
|
+
elsif !@report_server.nil?
|
3371
|
+
send_update_to_server
|
3372
|
+
end
|
3373
|
+
|
3374
|
+
unlock
|
3375
|
+
return ret_val
|
3376
|
+
end
|
3377
|
+
|
3378
|
+
def remove(requests=nil, options={})
|
3379
|
+
ret_val = 0
|
3380
|
+
lock
|
3381
|
+
|
3382
|
+
packages_to_remove = nil
|
3383
|
+
if requests
|
3384
|
+
packages_to_remove = []
|
3385
|
+
requests.each do |request|
|
3386
|
+
req = Tpkg::parse_request(request, @installed_directory)
|
3387
|
+
packages_to_remove.concat(installed_packages_that_meet_requirement(req))
|
3388
|
+
end
|
3389
|
+
else
|
3390
|
+
packages_to_remove = installed_packages_that_meet_requirement
|
3391
|
+
end
|
3392
|
+
|
3393
|
+
if packages_to_remove.empty?
|
3394
|
+
puts "No matching packages"
|
3395
|
+
unlock
|
3396
|
+
return false
|
3397
|
+
end
|
3398
|
+
|
3399
|
+
# If user want to remove all the dependent pkgs, then go ahead
|
3400
|
+
# and include them in our array of things to remove
|
3401
|
+
if options[:remove_all_dep]
|
3402
|
+
packages_to_remove |= get_dependents(packages_to_remove)
|
3403
|
+
elsif options[:remove_all_prereq]
|
3404
|
+
puts "Attemping to remove #{packages_to_remove.map do |pkg| pkg[:metadata][:filename] end} and all prerequisites."
|
3405
|
+
# Get list of dependency prerequisites
|
3406
|
+
ptr = packages_to_remove | get_prerequisites(packages_to_remove)
|
3407
|
+
pkg_files_to_remove = ptr.map { |pkg| pkg[:metadata][:filename] }
|
3408
|
+
|
3409
|
+
# see if any other packages depends on the ones we're about to remove
|
3410
|
+
# If so, we can't remove that package + any of its prerequisites
|
3411
|
+
non_removable_pkg_files = []
|
3412
|
+
metadata_for_installed_packages.each do |metadata|
|
3413
|
+
next if pkg_files_to_remove.include?(metadata[:filename])
|
3414
|
+
next if metadata[:dependencies].nil?
|
3415
|
+
metadata[:dependencies].each do |req|
|
3416
|
+
# We ignore native dependencies because there is no way a removal
|
3417
|
+
# can break a native dependency, we don't support removing native
|
3418
|
+
# packages.
|
3419
|
+
if req[:type] != :native && req[:type] != :native_installed
|
3420
|
+
iptmr = installed_packages_that_meet_requirement(req)
|
3421
|
+
if iptmr.all? { |pkg| pkg_files_to_remove.include?(pkg[:metadata][:filename]) }
|
3422
|
+
non_removable_pkg_files |= iptmr.map{ |pkg| pkg[:metadata][:filename]}
|
3423
|
+
non_removable_pkg_files |= get_prerequisites(iptmr).map{ |pkg| pkg[:metadata][:filename]}
|
3424
|
+
end
|
3425
|
+
end
|
3426
|
+
end
|
3427
|
+
end
|
3428
|
+
# Generate final list of packages that we should remove.
|
3429
|
+
packages_to_remove = {}
|
3430
|
+
ptr.each do | pkg |
|
3431
|
+
next if pkg[:source] == :native or pkg[:source] == :native_installed
|
3432
|
+
next if non_removable_pkg_files.include?(pkg[:metadata][:filename])
|
3433
|
+
packages_to_remove[pkg[:metadata][:filename]] = pkg
|
3434
|
+
end
|
3435
|
+
packages_to_remove = packages_to_remove.values
|
3436
|
+
if packages_to_remove.empty?
|
3437
|
+
raise "Can't remove request package because other packages depend on it."
|
3438
|
+
elsif !non_removable_pkg_files.empty?
|
3439
|
+
puts "Can't remove #{non_removable_pkg_files.inspect} because other packages depend on them."
|
3440
|
+
end
|
3441
|
+
# Check that this doesn't leave any dependencies unresolved
|
3442
|
+
elsif !options[:upgrade]
|
3443
|
+
pkg_files_to_remove = packages_to_remove.map { |pkg| pkg[:metadata][:filename] }
|
3444
|
+
metadata_for_installed_packages.each do |metadata|
|
3445
|
+
next if pkg_files_to_remove.include?(metadata[:filename])
|
3446
|
+
next if metadata[:dependencies].nil?
|
3447
|
+
metadata[:dependencies].each do |req|
|
3448
|
+
# We ignore native dependencies because there is no way a removal
|
3449
|
+
# can break a native dependency, we don't support removing native
|
3450
|
+
# packages.
|
3451
|
+
# FIXME: Should we also consider :native_installed?
|
3452
|
+
if req[:type] != :native
|
3453
|
+
if installed_packages_that_meet_requirement(req).all? { |pkg| pkg_files_to_remove.include?(pkg[:metadata][:filename]) }
|
3454
|
+
raise "Package #{metadata[:filename]} depends on #{req[:name]}"
|
3455
|
+
end
|
3456
|
+
end
|
3457
|
+
end
|
3458
|
+
end
|
3459
|
+
end
|
3460
|
+
|
3461
|
+
# Confirm with the user
|
3462
|
+
# upgrade does its own prompting
|
3463
|
+
if @@prompt && !options[:upgrade]
|
3464
|
+
puts "The following packages will be removed:"
|
3465
|
+
packages_to_remove.each do |pkg|
|
3466
|
+
puts pkg[:metadata][:filename]
|
3467
|
+
end
|
3468
|
+
unless Tpkg::confirm
|
3469
|
+
unlock
|
3470
|
+
return false
|
3471
|
+
end
|
3472
|
+
end
|
3473
|
+
|
3474
|
+
# Stop the services if there's init script
|
3475
|
+
if !options[:upgrade]
|
3476
|
+
packages_to_remove.each do |pkg|
|
3477
|
+
init_scripts_metadata = init_scripts(pkg[:metadata])
|
3478
|
+
if init_scripts_metadata && !init_scripts_metadata.empty?
|
3479
|
+
execute_init_for_package(pkg, 'stop')
|
3480
|
+
end
|
3481
|
+
end
|
3482
|
+
end
|
3483
|
+
|
3484
|
+
# Remove the packages
|
3485
|
+
packages_to_remove.each do |pkg|
|
3486
|
+
pkgname = pkg[:metadata][:name]
|
3487
|
+
package_file = File.join(@installed_directory, pkg[:metadata][:filename])
|
3488
|
+
|
3489
|
+
topleveldir = Tpkg::package_toplevel_directory(package_file)
|
3490
|
+
workdir = Tpkg::tempdir(topleveldir, @tmp_directory)
|
3491
|
+
system("#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{@tar} -C #{workdir} -xpf -")
|
3492
|
+
|
3493
|
+
# Run preremove script
|
3494
|
+
if File.exist?(File.join(workdir, 'tpkg', 'preremove'))
|
3495
|
+
pwd = Dir.pwd
|
3496
|
+
# chdir into the working directory so that the user can specify a
|
3497
|
+
# relative path to their file/script.
|
3498
|
+
Dir.chdir(File.join(workdir, 'tpkg'))
|
3499
|
+
|
3500
|
+
# Warn the user about non-executable files, as system will just
|
3501
|
+
# silently fail and exit if that's the case.
|
3502
|
+
if !File.executable?(File.join(workdir, 'tpkg', 'preremove'))
|
3503
|
+
warn "Warning: preremove script for #{File.basename(package_file)} is not executable, execution will likely fail"
|
3504
|
+
end
|
3505
|
+
if @force
|
3506
|
+
system(File.join(workdir, 'tpkg', 'preremove')) || warn("Warning: preremove for #{File.basename(package_file)} failed with exit value #{$?.exitstatus}")
|
3507
|
+
else
|
3508
|
+
system(File.join(workdir, 'tpkg', 'preremove')) || raise("Error: preremove for #{File.basename(package_file)} failed with exit value #{$?.exitstatus}")
|
3509
|
+
end
|
3510
|
+
|
3511
|
+
# Switch back to our previous directory
|
3512
|
+
Dir.chdir(pwd)
|
3513
|
+
end
|
3514
|
+
|
3515
|
+
# Remove any init scripts
|
3516
|
+
init_links(pkg[:metadata]).each do |link, init_script|
|
3517
|
+
# The link we ended up making when we unpacked the package could
|
3518
|
+
# be any of a series (see the code in unpack for the reasoning),
|
3519
|
+
# we need to check them all.
|
3520
|
+
links = [link]
|
3521
|
+
links.concat((1..9).to_a.map { |i| link + i.to_s })
|
3522
|
+
links.each do |l|
|
3523
|
+
if File.symlink?(l) && File.readlink(l) == init_script
|
3524
|
+
begin
|
3525
|
+
File.delete(l)
|
3526
|
+
rescue Errno::EPERM
|
3527
|
+
if Process.euid == 0
|
3528
|
+
raise
|
3529
|
+
else
|
3530
|
+
warn "Failed to remove init script for #{File.basename(package_file)}, probably due to lack of root privileges"
|
3531
|
+
end
|
3532
|
+
end
|
3533
|
+
end
|
3534
|
+
end
|
3535
|
+
end
|
3536
|
+
|
3537
|
+
# Remove any crontabs
|
3538
|
+
crontab_destinations(pkg[:metadata]).each do |crontab, destination|
|
3539
|
+
begin
|
3540
|
+
if destination[:link]
|
3541
|
+
# The link we ended up making when we unpacked the package could
|
3542
|
+
# be any of a series (see the code in unpack for the reasoning),
|
3543
|
+
# we need to check them all.
|
3544
|
+
links = [destination[:link]]
|
3545
|
+
links.concat((1..9).to_a.map { |i| destination[:link] + i.to_s })
|
3546
|
+
links.each do |l|
|
3547
|
+
if File.symlink?(l) && File.readlink(l) == crontab
|
3548
|
+
begin
|
3549
|
+
File.delete(l)
|
3550
|
+
rescue Errno::EPERM
|
3551
|
+
if Process.euid == 0
|
3552
|
+
raise
|
3553
|
+
else
|
3554
|
+
warn "Failed to remove crontab for #{File.basename(package_file)}, probably due to lack of root privileges"
|
3555
|
+
end
|
3556
|
+
end
|
3557
|
+
end
|
3558
|
+
end
|
3559
|
+
elsif destination[:file]
|
3560
|
+
if File.exist?(destination[:file])
|
3561
|
+
tmpfile = Tempfile.new(File.basename(destination[:file]), File.dirname(destination[:file]))
|
3562
|
+
# Match permissions and ownership of current crontab
|
3563
|
+
st = File.stat(destination[:file])
|
3564
|
+
File.chmod(st.mode & 07777, tmpfile.path)
|
3565
|
+
File.chown(st.uid, st.gid, tmpfile.path)
|
3566
|
+
# Remove section associated with this package
|
3567
|
+
skip = false
|
3568
|
+
IO.foreach(destination[:file]) do |line|
|
3569
|
+
if line == "### TPKG START - #{@base} - #{File.basename(package_file)}\n"
|
3570
|
+
skip = true
|
3571
|
+
elsif line == "### TPKG END - #{@base} - #{File.basename(package_file)}\n"
|
3572
|
+
skip = false
|
3573
|
+
elsif !skip
|
3574
|
+
tmpfile.write(line)
|
3575
|
+
end
|
3576
|
+
end
|
3577
|
+
tmpfile.close
|
3578
|
+
File.rename(tmpfile.path, destination[:file])
|
3579
|
+
# FIXME: On Solaris we should bounce cron or use the crontab
|
3580
|
+
# command, otherwise cron won't pick up the changes
|
3581
|
+
end
|
3582
|
+
end
|
3583
|
+
rescue Errno::EPERM
|
3584
|
+
# If removing the crontab fails due to permission problems and
|
3585
|
+
# we're not running as root just warn the user, allowing folks
|
3586
|
+
# to run tpkg as a non-root user with reduced functionality.
|
3587
|
+
if Process.euid == 0
|
3588
|
+
raise
|
3589
|
+
else
|
3590
|
+
warn "Failed to remove crontab for #{File.basename(package_file)}, probably due to lack of root privileges"
|
3591
|
+
end
|
3592
|
+
end
|
3593
|
+
end
|
3594
|
+
|
3595
|
+
# Run any externals
|
3596
|
+
pkg[:metadata][:externals].each do |external|
|
3597
|
+
if !options[:externals_to_skip] || !options[:externals_to_skip].include?(external)
|
3598
|
+
run_external(pkg[:metadata][:filename], :remove, external[:name], external[:data])
|
3599
|
+
end
|
3600
|
+
end if pkg[:metadata][:externals]
|
3601
|
+
|
3602
|
+
# Remove files
|
3603
|
+
files_to_remove = conflicting_files(package_file, CHECK_REMOVE)
|
3604
|
+
# Reverse the order of the files, as directories will appear first
|
3605
|
+
# in the listing but we want to remove any files in them before
|
3606
|
+
# trying to remove the directory.
|
3607
|
+
files_to_remove.reverse.each do |file|
|
3608
|
+
begin
|
3609
|
+
if !File.directory?(file)
|
3610
|
+
File.delete(file)
|
3611
|
+
else
|
3612
|
+
begin
|
3613
|
+
Dir.delete(file)
|
3614
|
+
rescue SystemCallError => e
|
3615
|
+
# Directory isn't empty
|
3616
|
+
#puts e.message
|
3617
|
+
end
|
3618
|
+
end
|
3619
|
+
rescue Errno::ENOENT
|
3620
|
+
warn "File #{file} from package #{File.basename(package_file)} missing during remove"
|
3621
|
+
end
|
3622
|
+
end
|
3623
|
+
|
3624
|
+
# Run postremove script
|
3625
|
+
if File.exist?(File.join(workdir, 'tpkg', 'postremove'))
|
3626
|
+
pwd = Dir.pwd
|
3627
|
+
# chdir into the working directory so that the user can specify a
|
3628
|
+
# relative path to their file/script.
|
3629
|
+
Dir.chdir(File.join(workdir, 'tpkg'))
|
3630
|
+
|
3631
|
+
# Warn the user about non-executable files, as system will just
|
3632
|
+
# silently fail and exit if that's the case.
|
3633
|
+
if !File.executable?(File.join(workdir, 'tpkg', 'postremove'))
|
3634
|
+
warn "Warning: postremove script for #{File.basename(package_file)} is not executable, execution will likely fail"
|
3635
|
+
end
|
3636
|
+
# Note this only warns the user if the postremove fails, it does
|
3637
|
+
# not raise an exception like we do if preremove fails. Raising
|
3638
|
+
# an exception would leave the package's files removed but the
|
3639
|
+
# package still registered as installed, which does not seem
|
3640
|
+
# desirable. We could reinstall the package's files and raise an
|
3641
|
+
# exception, but this seems the best approach to me.
|
3642
|
+
system(File.join(workdir, 'tpkg', 'postremove')) || warn("Warning: postremove for #{File.basename(package_file)} failed with exit value #{$?.exitstatus}")
|
3643
|
+
ret_val = POSTREMOVE_ERR if $?.exitstatus > 0
|
3644
|
+
|
3645
|
+
# Switch back to our previous directory
|
3646
|
+
Dir.chdir(pwd)
|
3647
|
+
end
|
3648
|
+
|
3649
|
+
File.delete(package_file)
|
3650
|
+
|
3651
|
+
# delete metadata dir of this package
|
3652
|
+
package_metadata_dir = File.join(@metadata_directory, File.basename(package_file, File.extname(package_file)))
|
3653
|
+
FileUtils.rm_rf(package_metadata_dir)
|
3654
|
+
|
3655
|
+
# Cleanup
|
3656
|
+
FileUtils.rm_rf(workdir)
|
3657
|
+
end
|
3658
|
+
|
3659
|
+
send_update_to_server unless @report_server.nil? || options[:upgrade]
|
3660
|
+
unlock
|
3661
|
+
return ret_val
|
3662
|
+
end
|
3663
|
+
|
3664
|
+
def verify_file_metadata(requests)
|
3665
|
+
results = {}
|
3666
|
+
packages = []
|
3667
|
+
# parse request to determine what packages the user wants to verify
|
3668
|
+
requests.each do |request|
|
3669
|
+
req = Tpkg::parse_request(request)
|
3670
|
+
packages.concat(installed_packages_that_meet_requirement(req).collect { |pkg| pkg[:metadata][:filename] })
|
3671
|
+
end
|
3672
|
+
|
3673
|
+
# loop through each package, and verify checksum, owner, group and perm of each file that was installed
|
3674
|
+
packages.each do | package_file |
|
3675
|
+
puts "Verifying #{package_file}"
|
3676
|
+
package_full_name = File.basename(package_file, File.extname(package_file))
|
3677
|
+
|
3678
|
+
# Extract checksum.xml from the package
|
3679
|
+
checksum_xml = nil
|
3680
|
+
|
3681
|
+
# get file_metadata.xml from the installed package
|
3682
|
+
file_metadata_bin = File.join(@metadata_directory, package_full_name, 'file_metadata.bin')
|
3683
|
+
file_metadata_yml = File.join(@metadata_directory, package_full_name, 'file_metadata.yml')
|
3684
|
+
file_metadata_xml = File.join(@metadata_directory, package_full_name, 'file_metadata.xml')
|
3685
|
+
if File.exist?(file_metadata_bin)
|
3686
|
+
file_metadata = FileMetadata.new(File.read(file_metadata_bin), 'bin')
|
3687
|
+
elsif File.exist?(file_metadata_yml)
|
3688
|
+
file_metadata = FileMetadata.new(File.read(file_metadata_yml), 'yml')
|
3689
|
+
elsif File.exist?(file_metadata_xml)
|
3690
|
+
file_metadata = FileMetadata.new(File.read(file_metadata_xml), 'xml')
|
3691
|
+
else
|
3692
|
+
errors = []
|
3693
|
+
errors << "Can't find file_metadata.xml or file_metadata.yml file. Most likely this is because the package was created before the verify feature was added"
|
3694
|
+
results[package_file] = errors
|
3695
|
+
return results
|
3696
|
+
end
|
3697
|
+
|
3698
|
+
# verify installed files match their checksum
|
3699
|
+
file_metadata[:files].each do |file|
|
3700
|
+
errors = []
|
3701
|
+
gid_expected, uid_expected, perms_expected, chksum_expected = nil
|
3702
|
+
fp = file[:path]
|
3703
|
+
|
3704
|
+
# get expected checksum. For files that were encrypted, we're interested in the
|
3705
|
+
# checksum of the decrypted version
|
3706
|
+
if file[:checksum]
|
3707
|
+
chksum_expected = file[:checksum][:digests].first[:value]
|
3708
|
+
file[:checksum][:digests].each do | digest |
|
3709
|
+
if digest[:decrypted] == true
|
3710
|
+
chksum_expected = digest[:value].to_s
|
3711
|
+
end
|
3712
|
+
end
|
3713
|
+
end
|
3714
|
+
|
3715
|
+
# get expected acl values
|
3716
|
+
if file[:uid]
|
3717
|
+
uid_expected = file[:uid].to_i
|
3718
|
+
end
|
3719
|
+
if file[:gid]
|
3720
|
+
gid_expected = file[:gid].to_i
|
3721
|
+
end
|
3722
|
+
if file[:perms]
|
3723
|
+
perms_expected = file[:perms].to_s
|
3724
|
+
end
|
3725
|
+
|
3726
|
+
# normalize file path
|
3727
|
+
if file[:relocatable] == true
|
3728
|
+
fp = File.join(@base, fp)
|
3729
|
+
else
|
3730
|
+
fp = File.join(@file_system_root, fp)
|
3731
|
+
end
|
3732
|
+
|
3733
|
+
# can't handle symlink
|
3734
|
+
if File.symlink?(fp)
|
3735
|
+
next
|
3736
|
+
end
|
3737
|
+
|
3738
|
+
# check if file exist
|
3739
|
+
if !File.exists?(fp)
|
3740
|
+
errors << "File is missing"
|
3741
|
+
else
|
3742
|
+
# get actual values
|
3743
|
+
#chksum_actual = Digest::SHA256.file(fp).hexdigest if File.file?(fp)
|
3744
|
+
chksum_actual = Digest::SHA256.hexdigest(File.read(fp)) if File.file?(fp)
|
3745
|
+
uid_actual = File.stat(fp).uid
|
3746
|
+
gid_actual = File.stat(fp).gid
|
3747
|
+
perms_actual = File.stat(fp).mode.to_s(8)
|
3748
|
+
end
|
3749
|
+
|
3750
|
+
if !chksum_expected.nil? && !chksum_actual.nil? && chksum_expected != chksum_actual
|
3751
|
+
errors << "Checksum doesn't match (Expected: #{chksum_expected}, Actual: #{chksum_actual}"
|
3752
|
+
end
|
3753
|
+
|
3754
|
+
if !uid_expected.nil? && !uid_actual.nil? && uid_expected != uid_actual
|
3755
|
+
errors << "uid doesn't match (Expected: #{uid_expected}, Actual: #{uid_actual}) "
|
3756
|
+
end
|
3757
|
+
|
3758
|
+
if !gid_expected.nil? && !gid_actual.nil? && gid_expected != gid_actual
|
3759
|
+
errors << "gid doesn't match (Expected: #{gid_expected}, Actual: #{gid_actual})"
|
3760
|
+
end
|
3761
|
+
|
3762
|
+
if !perms_expected.nil? && !perms_actual.nil? && perms_expected != perms_actual
|
3763
|
+
errors << "perms doesn't match (Expected: #{perms_expected}, Actual: #{perms_actual})"
|
3764
|
+
end
|
3765
|
+
|
3766
|
+
results[fp] = errors
|
3767
|
+
end
|
3768
|
+
end
|
3769
|
+
return results
|
3770
|
+
end
|
3771
|
+
|
3772
|
+
def execute_init(requests, action)
|
3773
|
+
ret_val = 0
|
3774
|
+
packages_to_execute_on = []
|
3775
|
+
if requests.nil?
|
3776
|
+
packages_to_execute_on = installed_packages_that_meet_requirement(nil)
|
3777
|
+
else
|
3778
|
+
requests.each do |request|
|
3779
|
+
req = Tpkg::parse_request(request)
|
3780
|
+
packages_to_execute_on.concat(installed_packages_that_meet_requirement(req))
|
3781
|
+
end
|
3782
|
+
end
|
3783
|
+
|
3784
|
+
packages_to_execute_on.each do |pkg|
|
3785
|
+
ret_val |= execute_init_for_package(pkg, action)
|
3786
|
+
end
|
3787
|
+
return ret_val
|
3788
|
+
end
|
3789
|
+
|
3790
|
+
def execute_init_for_package(pkg, action)
|
3791
|
+
ret_val = 0
|
3792
|
+
init_scripts_metadata = init_scripts(pkg[:metadata])
|
3793
|
+
|
3794
|
+
# warn if there's no init script and then return
|
3795
|
+
if init_scripts_metadata.nil? || init_scripts_metadata.empty?
|
3796
|
+
warn "Warning: There is no init script for #{pkg[:metadata][:name]}"
|
3797
|
+
return 1
|
3798
|
+
end
|
3799
|
+
|
3800
|
+
# convert the init scripts metadata to an array of { path => value, start => value}
|
3801
|
+
# so that we can order them based on their start value. This is necessary because
|
3802
|
+
# we need to execute the init scripts in correct order.
|
3803
|
+
init_scripts = []
|
3804
|
+
init_scripts_metadata.each do | installed_path, init_info |
|
3805
|
+
init = {}
|
3806
|
+
init[:path] = installed_path
|
3807
|
+
init[:start] = init_info[:init][:start] || 0
|
3808
|
+
init_scripts << init
|
3809
|
+
end
|
3810
|
+
|
3811
|
+
# Reverse order if doing stop.
|
3812
|
+
if action == "stop"
|
3813
|
+
ordered_init_scripts = init_scripts.sort{ |a,b| b[:start] <=> a[:start] }
|
3814
|
+
else
|
3815
|
+
ordered_init_scripts = init_scripts.sort{ |a,b| a[:start] <=> b[:start] }
|
3816
|
+
end
|
3817
|
+
|
3818
|
+
ordered_init_scripts.each do |init_script|
|
3819
|
+
installed_path = init_script[:path]
|
3820
|
+
system("#{installed_path} #{action}")
|
3821
|
+
ret_val = INITSCRIPT_ERR if $?.exitstatus > 0
|
3822
|
+
end
|
3823
|
+
return ret_val
|
3824
|
+
end
|
3825
|
+
|
3826
|
+
# We can't safely calculate a set of dependencies and install the
|
3827
|
+
# resulting set of packages if another user is manipulating the installed
|
3828
|
+
# packages at the same time. These methods lock and unlock the package
|
3829
|
+
# system so that only one user makes changes at a time.
|
3830
|
+
def lock
|
3831
|
+
if @locks > 0
|
3832
|
+
@locks += 1
|
3833
|
+
return
|
3834
|
+
end
|
3835
|
+
if File.directory?(@lock_directory)
|
3836
|
+
if @lockforce
|
3837
|
+
warn "Forcing lock removal"
|
3838
|
+
FileUtils.rm_rf(@lock_directory)
|
3839
|
+
else
|
3840
|
+
# Remove old lock files on the assumption that they were left behind
|
3841
|
+
# by a previous failed run
|
3842
|
+
if File.mtime(@lock_directory) < Time.at(Time.now - 60 * 60 * 2)
|
3843
|
+
warn "Lock is more than 2 hours old, removing"
|
3844
|
+
FileUtils.rm_rf(@lock_directory)
|
3845
|
+
end
|
3846
|
+
end
|
3847
|
+
end
|
3848
|
+
begin
|
3849
|
+
Dir.mkdir(@lock_directory)
|
3850
|
+
File.open(@lock_pid_file, 'w') { |file| file.puts($$) }
|
3851
|
+
@locks = 1
|
3852
|
+
rescue Errno::EEXIST
|
3853
|
+
lockpid = ''
|
3854
|
+
begin
|
3855
|
+
File.open(@lock_pid_file) { |file| lockpid = file.gets.chomp }
|
3856
|
+
rescue Errno::ENOENT
|
3857
|
+
end
|
3858
|
+
|
3859
|
+
# check that the process is actually running
|
3860
|
+
# if not, clean up old lock and attemp to obtain lock again
|
3861
|
+
if Tpkg::process_running?(lockpid)
|
3862
|
+
raise "tpkg repository locked by another process (with PID #{lockpid})"
|
3863
|
+
else
|
3864
|
+
FileUtils.rm_rf(@lock_directory)
|
3865
|
+
lock
|
3866
|
+
end
|
3867
|
+
end
|
3868
|
+
end
|
3869
|
+
|
3870
|
+
def unlock
|
3871
|
+
if @locks == 0
|
3872
|
+
warn "unlock called but not locked, that probably shouldn't happen"
|
3873
|
+
return
|
3874
|
+
end
|
3875
|
+
@locks -= 1
|
3876
|
+
if @locks == 0
|
3877
|
+
FileUtils.rm_rf(@lock_directory)
|
3878
|
+
end
|
3879
|
+
end
|
3880
|
+
|
3881
|
+
# TODO: update server side to accept yaml data
|
3882
|
+
def send_update_to_server
|
3883
|
+
metadata = metadata_for_installed_packages.collect{|metadata| metadata.hash}
|
3884
|
+
yml = YAML.dump(metadata)
|
3885
|
+
begin
|
3886
|
+
update_uri = URI.parse("#{@report_server}")
|
3887
|
+
http = Tpkg::gethttp(update_uri)
|
3888
|
+
request = {"yml"=>URI.escape(yml), "client"=>Facter['fqdn'].value}
|
3889
|
+
post = Net::HTTP::Post.new(update_uri.path)
|
3890
|
+
post.set_form_data(request)
|
3891
|
+
response = http.request(post)
|
3892
|
+
|
3893
|
+
case response
|
3894
|
+
when Net::HTTPSuccess
|
3895
|
+
# puts "Response from server:\n'#{response.body}'"
|
3896
|
+
puts "Successfully send update to reporter server"
|
3897
|
+
else
|
3898
|
+
$stderr.puts response.body
|
3899
|
+
#response.error!
|
3900
|
+
# just ignore error and give user warning
|
3901
|
+
puts "Failed to send update to reporter server"
|
3902
|
+
end
|
3903
|
+
rescue
|
3904
|
+
puts "Failed to send update to reporter server"
|
3905
|
+
end
|
3906
|
+
end
|
3907
|
+
|
3908
|
+
# Build a dependency map of currently installed packages
|
3909
|
+
# For example, if we have pkgB and pkgC which depends on pkgA, then
|
3910
|
+
# the dependency map would look like this:
|
3911
|
+
# "pkgA.tpkg" => [{pkgB metadata}, {pkgC metadata}]
|
3912
|
+
def get_dependency_mapping
|
3913
|
+
dependency_mapping = {}
|
3914
|
+
installed_packages.each do | pkg |
|
3915
|
+
metadata = pkg[:metadata]
|
3916
|
+
|
3917
|
+
# Get list of pkgs that this pkg depends on
|
3918
|
+
next if metadata[:dependencies].nil?
|
3919
|
+
depended_on = []
|
3920
|
+
metadata[:dependencies].each do |req|
|
3921
|
+
next if req[:type] == :native
|
3922
|
+
depended_on |= installed_packages_that_meet_requirement(req)
|
3923
|
+
end
|
3924
|
+
|
3925
|
+
# populate the depencency map
|
3926
|
+
depended_on.each do | req_pkg |
|
3927
|
+
dependency_mapping[req_pkg[:metadata][:filename]] = [] if dependency_mapping[req_pkg[:metadata][:filename]].nil?
|
3928
|
+
dependency_mapping[req_pkg[:metadata][:filename]] << pkg
|
3929
|
+
end
|
3930
|
+
end
|
3931
|
+
return dependency_mapping
|
3932
|
+
end
|
3933
|
+
|
3934
|
+
# Given a list of packages, return a list of dependents packages
|
3935
|
+
def get_dependents(pkgs)
|
3936
|
+
dependents = []
|
3937
|
+
to_check = pkgs.map { |pkg| pkg[:metadata][:filename] }
|
3938
|
+
dependency = get_dependency_mapping
|
3939
|
+
while pkgfile = to_check.pop
|
3940
|
+
pkgs = dependency[pkgfile.to_s]
|
3941
|
+
next if pkgs.nil?
|
3942
|
+
dependents |= pkgs
|
3943
|
+
to_check |= pkgs.map { |pkg| pkg[:metadata][:filename] }
|
3944
|
+
end
|
3945
|
+
return dependents
|
3946
|
+
end
|
3947
|
+
|
3948
|
+
# Given a list of packages, return a list of all their prerequisite dependencies
|
3949
|
+
# Example: If pkgA depends on pkgB, and pkgB depends on pkgC, then calling this
|
3950
|
+
# method on pkgA will returns pkgB and pkgC
|
3951
|
+
# Assumption: There is no cyclic dependency
|
3952
|
+
def get_prerequisites(pkgs)
|
3953
|
+
pre_reqs = []
|
3954
|
+
to_check = pkgs.clone
|
3955
|
+
while pkg = to_check.pop
|
3956
|
+
next if pkg[:metadata][:dependencies].nil?
|
3957
|
+
pkg[:metadata][:dependencies].each do | dep |
|
3958
|
+
pre_req = installed_packages_that_meet_requirement(dep)
|
3959
|
+
pre_reqs |= pre_req
|
3960
|
+
to_check |= pre_req
|
3961
|
+
end
|
3962
|
+
end
|
3963
|
+
return pre_reqs
|
3964
|
+
end
|
3965
|
+
end
|
3966
|
+
|