tpkg 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|