tpkg 1.18.2 → 1.19.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 CHANGED
@@ -4,7 +4,8 @@ spec = Gem::Specification.new do |s|
4
4
  s.summary = 'tpkg Application Packaging & Deployment'
5
5
  s.add_dependency('facter')
6
6
  s.add_dependency('net-ssh')
7
- s.version = '1.18.2'
7
+ s.add_dependency('kwalify')
8
+ s.version = '1.19.2'
8
9
  s.authors = ['Darren Dao', 'Jason Heiss']
9
10
  s.email = 'tpkg-users@lists.sourceforge.net'
10
11
  s.homepage = 'http://tpkg.sourceforge.net'
@@ -29,6 +29,8 @@ Usage: cpan2tpkg
29
29
  Extra dependencies to add to the package
30
30
  [--native-deps foo,1.0,1.9999,bar,4.5,4.5,blah,,,]
31
31
  Native dependencies to add to the package
32
+ [--force]
33
+ Force the install and packaging even if tests fail
32
34
  [--help]
33
35
  Show this message
34
36
 
@@ -42,6 +44,7 @@ my %extradeps = ();
42
44
  my $extradepsopts = '';
43
45
  my %nativedeps = ();
44
46
  my $nativedepsopts = '';
47
+ my $force;
45
48
  my $help;
46
49
 
47
50
  my $getopt = GetOptions(
@@ -49,6 +52,7 @@ my $getopt = GetOptions(
49
52
  'package-version=s' => \$pkgver,
50
53
  'extra-deps=s' => \$extradepsopts,
51
54
  'native-deps=s' => \$nativedepsopts,
55
+ 'force' => \$force,
52
56
  'help' => \$help);
53
57
 
54
58
  my $module = shift @ARGV;
@@ -131,7 +135,14 @@ CPAN::Shell->o('conf', 'mbuildpl_arg', "--destdir=$workdir");
131
135
  #CPAN::Shell->o('conf', 'prerequisites_policy', "ask");
132
136
 
133
137
  # Install the module
134
- $modobj->install;
138
+ if ($force)
139
+ {
140
+ CPAN::Shell->force('install', $module);
141
+ }
142
+ else
143
+ {
144
+ $modobj->install;
145
+ }
135
146
 
136
147
  # It is not nearly as straightforward as one might wish to get
137
148
  # ExtUtils::Installed to inspect only a specified directory structure and not
@@ -10,20 +10,9 @@ require 'shellwords'
10
10
  require 'tpkg'
11
11
  require 'facter'
12
12
 
13
- # Names of packages containing ruby and gems. Dependencies on these
14
- # will be added to generated packages, so that if a user installs a gem
15
- # then tpkg will pull in ruby and gems.
16
- RUBYDEPS = ['ruby']
17
- # T_RUBY_BASE = "/home/t/ruby"
18
- T_RUBY_BASE = "#{Tpkg::DEFAULT_BASE}/#{RUBYDEPS.first}".freeze
19
- DEFAULT_GEM_COMMAND = "#{T_RUBY_BASE}/bin/gem"
20
-
21
- # Figure out rubygems library from ruby.
22
- # There might be several versions so get the latest one. We're assuming that
23
- # the version directories are named appropriately under lib/ruby/site_ruby.
24
- versions = Dir.glob(File.join("#{T_RUBY_BASE}", "lib", "ruby", "site_ruby", "*"))
25
- versions.sort!
26
- DEFAULT_RUBYGEMS_PATH = versions[-1]
13
+ # We don't want to just use the first gem command in the user's PATH by
14
+ # default, as that may not be a tpkg gem. I.e. /usr/bin/gem on Mac OS X.
15
+ DEFAULT_GEM_COMMAND = "#{Tpkg::DEFAULT_BASE}/ruby-1.8/bin/gem"
27
16
 
28
17
  # Haven't found a Ruby method for creating temporary directories,
29
18
  # so create a temporary file and replace it with a directory.
@@ -45,6 +34,7 @@ end
45
34
  @buildopts = []
46
35
  @gemcmd = DEFAULT_GEM_COMMAND
47
36
  @rubygemspath = nil
37
+ @extrapkgname = nil
48
38
 
49
39
  opts = OptionParser.new
50
40
  opts.banner = 'Usage: gem2tpkg [options] GEMNAME1 GEMNAME2 ...'
@@ -92,33 +82,91 @@ end
92
82
  opts.on('--rubygems-path', '=PATH', 'Path to rubygems library') do |opt|
93
83
  @rubygemspath = opt
94
84
  end
85
+ opts.on('--extra-name', '=EXTRANAME', 'Extra string to add to package name') do |opt|
86
+ @extrapkgname = opt
87
+ end
95
88
  opts.on_tail("-h", "--help", "Show this message") do
96
89
  puts opts
97
90
  exit
98
91
  end
99
92
 
100
- # we now allow user to specifies multiple gems at the end of the command line arguments
93
+ # Allow user to specify multiple gems at the end of the command line
94
+ # arguments
101
95
  leftover = opts.parse(ARGV)
102
96
  @gems = leftover
103
- #if leftover.length == 1
104
- # @gem = leftover.shift
105
- #else
106
- # puts opts
107
- # exit
108
- #end
109
97
 
110
- # require the correct rubygems based on what the user specifies
111
- @rubygemspath ||= DEFAULT_RUBYGEMS_PATH
98
+ # Extract a few paths from gem
99
+ @geminstallpath = nil
100
+ @gembinpath = nil
101
+ @rubypath = nil
102
+ IO.popen("#{@gemcmd} environment") do |pipe|
103
+ pipe.each_line do |line|
104
+ if line =~ /INSTALLATION DIRECTORY:\s+(\S+)/
105
+ @geminstallpath = $1
106
+ end
107
+ if line =~ /EXECUTABLE DIRECTORY:\s+(\S+)/
108
+ @gembinpath = $1
109
+ end
110
+ if line =~ /RUBY EXECUTABLE:\s+(\S+)/
111
+ @rubypath = $1
112
+ end
113
+ end
114
+ end
115
+ if !$?.success?
116
+ abort 'gem environment failed'
117
+ end
118
+
119
+ # Find the right rubygems library if the user didn't request a specific
120
+ # one
121
+ if !@rubygemspath
122
+ cmd = "#{@rubypath} -e 'puts $:'"
123
+ IO.popen(cmd) do |pipe|
124
+ pipe.each_line do |line|
125
+ line.chomp!
126
+ if File.exist?(File.join(line, 'rubygems.rb'))
127
+ @rubygemspath = line
128
+ break
129
+ end
130
+ end
131
+ end
132
+ if !$?.success?
133
+ abort "#{cmd} failed"
134
+ end
135
+ end
136
+
137
+ # Require the correct rubygems based on what the user specifies
112
138
  $:.unshift @rubygemspath unless @rubygemspath.nil?
113
139
  require 'rubygems'
114
140
 
141
+ # Ask tpkg what package owns the gem executable that we're using so that
142
+ # we can add a dependency on that package to any packages we build.
143
+ @gemdep = []
144
+ # FIXME: Currently the reading of config files and whatnot is done in
145
+ # the tpkg executable. We should probably move that into the library so
146
+ # that other utilities like this one use an alternate base if the user
147
+ # has configured one, etc.
148
+ tpkg = Tpkg.new(:base => Tpkg::DEFAULT_BASE)
149
+ tpkg.files_for_installed_packages.each do |pkgfile, fip|
150
+ fip[:normalized].each do |file|
151
+ if file == @gemcmd
152
+ metadata = nil
153
+ # FIXME: Should add a metadata_for_installed_package method to the
154
+ # library
155
+ tpkg.metadata_for_installed_packages.each do |metadata|
156
+ if metadata[:filename] == pkgfile
157
+ @gemdep << metadata[:name]
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
115
164
  # Create the directory we want gem to install into
116
165
  @gemdir = tempdir('gem2tpkg')
117
166
  ENV['GEM_HOME'] = @gemdir
118
167
  ENV['GEM_PATH'] = @gemdir
119
168
 
120
169
  # Install the gem
121
- #geminst = [@gemcmd, 'install', @gems.join(" "), '--no-rdoc', '--no-ri']
122
170
  geminst = [@gemcmd, 'install', '--no-rdoc', '--no-ri'] | @gems
123
171
  if @gemver
124
172
  geminst << @gemver
@@ -147,7 +195,6 @@ def package(gem)
147
195
  puts "Packaging #{gem}"
148
196
 
149
197
  gemsdir = nil
150
- #globdirs = Dir.glob(File.join(@gemdir, 'gems', "#{gem}-*"))
151
198
  globdirs = Dir.glob(File.join(@gemdir, 'gems', "#{gem}-[0-9]*"))
152
199
  if globdirs.length == 1
153
200
  gemsdir = globdirs[0]
@@ -286,7 +333,7 @@ def package(gem)
286
333
 
287
334
  # The directory where we make our package
288
335
  pkgdir = tempdir('gem2tpkg')
289
- pkgbasedir = File.join(pkgdir, "/root/#{T_RUBY_BASE}/lib/ruby/gems/1.8")
336
+ pkgbasedir = File.join(pkgdir, "/root/#{@geminstallpath}")
290
337
  FileUtils.mkdir_p(pkgbasedir)
291
338
  pkggemdir = File.join(pkgbasedir, 'gems')
292
339
  FileUtils.mkdir_p(pkggemdir)
@@ -305,7 +352,7 @@ def package(gem)
305
352
  binfiles << File.join(@gemdir, 'bin', exec)
306
353
  end
307
354
  if !binfiles.empty?
308
- bindir = "#{pkgdir}/root/#{T_RUBY_BASE}/bin"
355
+ bindir = "#{pkgdir}/root/#{@gembinpath}"
309
356
  FileUtils.mkdir_p(bindir)
310
357
  binfiles.each do |binfile|
311
358
  FileUtils.cp(binfile, bindir, :preserve => true)
@@ -315,6 +362,15 @@ def package(gem)
315
362
  # Copy over the gemspec file
316
363
  FileUtils.cp(gemspecfile, pkgspecdir, :preserve => true)
317
364
 
365
+ pkgnamesuffix = ''
366
+ if @extrapkgname
367
+ pkgnamesuffix = '-' + @extrapkgname
368
+ elsif @gemcmd != DEFAULT_GEM_COMMAND
369
+ # If we're not using the default gem try to name the package in a way
370
+ # that indicates the alternate ruby/gem used
371
+ pkgnamesuffix = '-' + @gemdep.first.sub(/\W/, '')
372
+ end
373
+
318
374
  # Add tpkg.xml
319
375
  os = nil
320
376
  arch = nil
@@ -322,7 +378,7 @@ def package(gem)
322
378
  file.puts '<?xml version="1.0" encoding="UTF-8"?>'
323
379
  file.puts '<!DOCTYPE tpkg SYSTEM "http://tpkg.sourceforge.net/tpkg-1.0.dtd">'
324
380
  file.puts '<tpkg>'
325
- file.puts " <name>gem-#{gem}</name>"
381
+ file.puts " <name>gem-#{gem}#{pkgnamesuffix}</name>"
326
382
  file.puts " <version>#{gemspec.version.to_s}</version>"
327
383
  file.puts " <package_version>#{@pkgver}</package_version>"
328
384
  file.puts ' <maintainer>gem2tpkg</maintainer>'
@@ -341,11 +397,11 @@ def package(gem)
341
397
  end
342
398
  if !deps.empty? ||
343
399
  !@extradeps.empty? || !@nativedeps.empty? ||
344
- !RUBYDEPS.empty?
400
+ !@gemdep.empty?
345
401
  file.puts ' <dependencies>'
346
402
  deps.each do |depgem, depvers|
347
403
  file.puts ' <dependency>'
348
- file.puts " <name>gem-#{depgem}</name>"
404
+ file.puts " <name>gem-#{depgem}#{pkgnamesuffix}</name>"
349
405
  if depvers[:minimum_version]
350
406
  file.puts " <minimum_version>#{depvers[:minimum_version]}</minimum_version>"
351
407
  end
@@ -377,9 +433,9 @@ def package(gem)
377
433
  file.puts ' <native/>'
378
434
  file.puts ' </dependency>'
379
435
  end
380
- RUBYDEPS.each do |rubydep|
436
+ @gemdep.each do |gemdep|
381
437
  file.puts ' <dependency>'
382
- file.puts " <name>#{rubydep}</name>"
438
+ file.puts " <name>#{gemdep}</name>"
383
439
  file.puts ' </dependency>'
384
440
  end
385
441
  file.puts ' </dependencies>'
@@ -389,24 +445,11 @@ def package(gem)
389
445
 
390
446
  # Make package
391
447
  pkgfile = Tpkg::make_package(pkgdir)
392
- # If the package is OS-specific then rename the file to reflect that
393
- if os
394
- # Examples:
395
- # FreeBSD-7 -> freebsd7
396
- # RedHat-5 -> redhat5
397
- # CentOS-5 -> redhat5
398
- fileos = Tpkg::get_os.sub('CentOS', 'RedHat').downcase.sub('-', '')
399
- newpkgfile = File.join(
400
- File.dirname(pkgfile),
401
- "#{File.basename(pkgfile, '.tpkg')}-#{fileos}-#{arch}.tpkg")
402
- File.rename(pkgfile, newpkgfile)
403
- pkgfile = newpkgfile
404
- end
405
448
  pkgfiles << pkgfile
406
449
 
407
450
  # Cleanup
408
451
  FileUtils.rm_rf(pkgdir)
409
-
452
+
410
453
  @already_packaged << gem
411
454
  pkgfiles
412
455
  end
data/bin/tpkg CHANGED
@@ -329,10 +329,14 @@ if @deploy
329
329
  exit
330
330
  end
331
331
 
332
+ if @action_value.is_a?(Array)
333
+ @action_value.uniq!
334
+ end
335
+
332
336
  ret_val = 0
333
337
  case @action
334
338
  when :make
335
- pkgfile = Tpkg::make_package(@action_value, passphrase_callback)
339
+ pkgfile = Tpkg::make_package(@action_value, passphrase_callback, {:force => @force})
336
340
  if pkgfile
337
341
  puts "Package is #{pkgfile}"
338
342
  else
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/ruby -w
2
2
 
3
+ # Ensure we can find tpkg.rb
4
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
5
+
3
6
  # This script expects one argument, which is the tpkg.xml file
4
7
  # that you want to convert to yml format.
5
8
  # The resulting data is output to stdout
@@ -21,6 +21,13 @@ if File.directory?(tpkglibdir)
21
21
  $:.unshift(tpkglibdir)
22
22
  end
23
23
 
24
+ # We store this gem in our thirdparty directory. So we need to add it
25
+ # it to the search path
26
+ # This one is for when everything is installed
27
+ $:.unshift(File.join(File.dirname(__FILE__), 'thirdparty/kwalify-0.7.1/lib'))
28
+ # And this one for when we're in the svn directory structure
29
+ $:.unshift(File.join(File.dirname(File.dirname(__FILE__)), 'thirdparty/kwalify-0.7.1/lib'))
30
+
24
31
  begin
25
32
  # Try loading facter w/o gems first so that we don't introduce a
26
33
  # dependency on gems if it is not needed.
@@ -45,16 +52,20 @@ require 'versiontype' # Version
45
52
  require 'deployer'
46
53
  require 'set'
47
54
  require 'metadata'
55
+ require 'kwalify' # for validating yaml
48
56
 
49
57
  class Tpkg
50
58
 
51
- VERSION = '1.18.2'
59
+ VERSION = '1.19.2'
52
60
  CONFIGDIR = '/etc'
53
-
61
+
62
+ GENERIC_ERR = 1
54
63
  POSTINSTALL_ERR = 2
55
64
  POSTREMOVE_ERR = 3
56
65
  INITSCRIPT_ERR = 4
57
66
 
67
+ CONNECTION_TIMEOUT = 10
68
+
58
69
  attr_reader :installed_directory
59
70
 
60
71
  #
@@ -74,6 +85,8 @@ class Tpkg
74
85
  # Find GNU tar or bsdtar in ENV['PATH']
75
86
  # Raises an exception if a suitable tar cannot be found
76
87
  @@tar = nil
88
+ @@taroptions = ""
89
+ @@tartype = nil
77
90
  TARNAMES = ['tar', 'gtar', 'gnutar', 'bsdtar']
78
91
  def self.find_tar
79
92
  if !@@tar
@@ -83,7 +96,12 @@ class Tpkg
83
96
  if File.executable?(File.join(path, tarname))
84
97
  IO.popen("#{File.join(path, tarname)} --version 2>/dev/null") do |pipe|
85
98
  pipe.each_line do |line|
86
- if line.include?('GNU tar') || line.include?('bsdtar')
99
+ if line.include?('GNU tar')
100
+ @@tartype = 'gnu'
101
+ @@tar = File.join(path, tarname)
102
+ throw :tar_found
103
+ elsif line.include?('bsdtar')
104
+ @@tartype = 'bsd'
87
105
  @@tar = File.join(path, tarname)
88
106
  throw :tar_found
89
107
  end
@@ -96,6 +114,15 @@ class Tpkg
96
114
  raise "Unable to find GNU tar or bsdtar in PATH"
97
115
  end
98
116
  end
117
+ # bsdtar uses pax format by default. This format allows for vendor extensions, such
118
+ # as the SCHILY.* extensions which were introduced by star). bsdtar actually uses
119
+ # these extensions. These extension headers includde useful, but not vital information.
120
+ # gnu tar should just ignore them and gives a warning. This is what the latest gnu tar
121
+ # will do. However, on older gnu tar, it only threw an error at the end. The work
122
+ # around is to explicitly tell gnu tar to ignore those extensions.
123
+ if @@tartype == 'gnu'
124
+ @@taroptions = "--pax-option='delete=SCHILY.*'"
125
+ end
99
126
  @@tar.dup
100
127
  end
101
128
  def self.clear_cached_tar
@@ -190,8 +217,7 @@ class Tpkg
190
217
  end
191
218
 
192
219
  # 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)
220
+ def self.make_package(pkgsrcdir, passphrase=nil, options = {})
195
221
  pkgfile = nil
196
222
 
197
223
  # Make a working directory
@@ -225,23 +251,40 @@ class Tpkg
225
251
  # code (tar) ever touch the user's files.
226
252
  system("#{find_tar} -C #{pkgsrcdir} -cf - . | #{find_tar} -C #{tpkgdir} -xpf -") || raise("Package content copy failed")
227
253
 
254
+ # check metadata file
255
+ errors = []
228
256
  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')
257
+ metadata_file = File.join(tpkgdir, 'tpkg.yml')
258
+ metadata_format = 'yml'
231
259
  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')
260
+ metadata_file = File.join(tpkgdir, 'tpkg.xml')
261
+ metadata_format = 'xml'
234
262
  else
235
263
  raise 'Your source directory does not contain the metadata configuration file.'
236
264
  end
265
+ metadata_text = File.read(metadata_file)
266
+ metadata = Metadata.new(metadata_text, metadata_format)
237
267
 
238
- metadata.verify_required_fields
268
+ # This is the directory where we put our dtd/schema for validating
269
+ # the metadata file
270
+ if File.exist?(File.join(CONFIGDIR, 'tpkg', 'schema'))
271
+ schema_dir = File.join(CONFIGDIR, 'tpkg', 'schema')
272
+ else # This is for when we're in developement mode or when installed as gem
273
+ schema_dir = File.join(File.dirname(File.dirname(__FILE__)), "schema")
274
+ end
275
+ errors = metadata.validate(schema_dir)
276
+ if errors && !errors.empty?
277
+ puts "Bad metadata file. Possible error(s):"
278
+ errors.each {|e| puts e }
279
+ raise "Failed to create package." unless options[:force]
280
+ end
239
281
 
240
282
  # file_metadata.yml hold information for files that are installed
241
283
  # by the package. For example, checksum, path, relocatable or not, etc.
242
284
  File.open(File.join(tpkgdir, "file_metadata.bin"), "w") do |file|
243
285
  filemetadata = get_filemetadata_from_directory(tpkgdir)
244
- Marshal::dump(filemetadata.to_hash, file)
286
+ data = filemetadata.to_hash.recursively{|h| h.stringify_keys }
287
+ Marshal::dump(data, file)
245
288
  # YAML::dump(filemetadata.to_hash, file)
246
289
  end
247
290
 
@@ -259,6 +302,19 @@ class Tpkg
259
302
  raise "File #{tpkg_path} referenced in tpkg.yml but not found"
260
303
  end
261
304
 
305
+ # check permission/ownership of crontab files
306
+ if tpkgfile[:crontab]
307
+ data = {:actual_file => working_path, :metadata => metadata, :file_metadata => tpkgfile}
308
+ perms, uid, gid = predict_file_perms_and_ownership(data)
309
+ # crontab needs to be owned by root, and is not writable by group or others
310
+ if uid != 0
311
+ warn "Warning: Your cron jobs in \"#{tpkgfile[:path]}\" might fail to run because the file is not owned by root."
312
+ end
313
+ if (perms & 0022) != 0
314
+ warn "Warning: Your cron jobs in \"#{tpkgfile[:path]}\" might fail to run because the file is writable by group and/or others."
315
+ end
316
+ end
317
+
262
318
  # Encrypt any files marked for encryption
263
319
  if tpkgfile[:encrypt]
264
320
  if tpkgfile[:encrypt] == 'precrypt'
@@ -323,8 +379,12 @@ class Tpkg
323
379
  toplevel = nil
324
380
  # FIXME: This is so lame, to read the whole package to get the
325
381
  # first filename. Blech.
326
- IO.popen("#{find_tar} -tf #{package_file}") do |pipe|
327
- toplevel = pipe.gets.chomp
382
+ IO.popen("#{find_tar} -tf #{package_file} #{@@taroptions}") do |pipe|
383
+ toplevel = pipe.gets
384
+ if toplevel.nil?
385
+ raise "Package directory structure of #{package_file} unexpected. Unable to get top level."
386
+ end
387
+ toplevel.chomp!
328
388
  # Avoid SIGPIPE, if we don't sink the rest of the output from tar
329
389
  # then tar ends up getting SIGPIPE when it tries to write to the
330
390
  # closed pipe and exits with error, which causes us to throw an
@@ -423,7 +483,7 @@ class Tpkg
423
483
  topleveldir = package_toplevel_directory(package_file)
424
484
  # Extract checksum.xml from the package
425
485
  checksum_xml = nil
426
- IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'checksum.xml')}") do |pipe|
486
+ IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'checksum.xml')} #{@@taroptions}") do |pipe|
427
487
  checksum_xml = REXML::Document.new(pipe.read)
428
488
  end
429
489
  if !$?.success?
@@ -448,7 +508,7 @@ class Tpkg
448
508
  raise("Unrecognized checksum algorithm #{checksum.elements['algorithm']}")
449
509
  end
450
510
  # 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|
511
+ IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} #{@@taroptions}") do |pipe|
452
512
  # Package files can be quite large, so we digest the package in
453
513
  # chunks. A survey of the Internet turns up someone who tested
454
514
  # various chunk sizes on various platforms and found 4k to be
@@ -880,7 +940,7 @@ class Tpkg
880
940
  files[:root] = []
881
941
  files[:reloc] = []
882
942
  topleveldir = package_toplevel_directory(package_file)
883
- IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{find_tar} -tf -") do |pipe|
943
+ IO.popen("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} #{@@taroptions}| #{find_tar} #{@@taroptions} -tf -") do |pipe|
884
944
  pipe.each do |file|
885
945
  file.chomp!
886
946
  if file =~ Regexp.new(File.join('tpkg', 'root'))
@@ -1007,6 +1067,7 @@ class Tpkg
1007
1067
  #
1008
1068
  # servers is an array or a callback that list the remote servers where we want to deploy to
1009
1069
  def self.deploy(deploy_params, deploy_options, servers)
1070
+ servers.uniq!
1010
1071
  deployer = Deployer.new(deploy_options)
1011
1072
  deployer.deploy(deploy_params, servers)
1012
1073
  end
@@ -1043,7 +1104,7 @@ class Tpkg
1043
1104
  begin
1044
1105
  topleveldir = Tpkg::package_toplevel_directory(package_file)
1045
1106
  workdir = Tpkg::tempdir(topleveldir)
1046
- system("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{find_tar} -C #{workdir} -xpf -")
1107
+ system("#{find_tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} #{@@taroptions}| #{find_tar} #{@@taroptions} -C #{workdir} -xpf -")
1047
1108
 
1048
1109
  if File.exist?(File.join(workdir,"tpkg", "tpkg.yml"))
1049
1110
  metadata_file = File.join(workdir,"tpkg", "tpkg.yml")
@@ -1060,6 +1121,47 @@ class Tpkg
1060
1121
  end
1061
1122
  return result
1062
1123
  end
1124
+
1125
+ # The only restriction right now is that the file doesn't begin with "."
1126
+ def self.valid_pkg_filename?(filename)
1127
+ return File.basename(filename) !~ /^\./
1128
+ end
1129
+
1130
+ # helper method for predicting the permissions and ownership of a file that
1131
+ # will be installed by tpkg. This is done by looking at:
1132
+ # 1) its current perms & ownership
1133
+ # 2) the file_defaults settings of the metadata file
1134
+ # 3) the explicitly defined settings in the corresponding file section of the metadata file
1135
+ def self.predict_file_perms_and_ownership(data)
1136
+ perms = nil
1137
+ uid = nil
1138
+ gid = nil
1139
+
1140
+ # get current permission and ownership
1141
+ if data[:actual_file]
1142
+ stat = File.stat(data[:actual_file])
1143
+ perms = stat.mode
1144
+ uid = stat.uid
1145
+ gid = stat.gid
1146
+ end
1147
+
1148
+ # get default permission and ownership
1149
+ metadata = data[:metadata]
1150
+ if (metadata && metadata[:files] && metadata[:files][:file_defaults] && metadata[:files][:file_defaults][:posix])
1151
+ uid = Tpkg::lookup_uid(metadata[:files][:file_defaults][:posix][:owner]) if metadata[:files][:file_defaults][:posix][:owner]
1152
+ gid = Tpkg::lookup_uid(metadata[:files][:file_defaults][:posix][:group]) if metadata[:files][:file_defaults][:posix][:group]
1153
+ perms = metadata[:files][:file_defaults][:posix][:perms] if metadata[:files][:file_defaults][:posix][:perms]
1154
+ end
1155
+
1156
+ # get explicitly defined permission and ownership
1157
+ file_metadata = data[:file_metadata]
1158
+ if file_metadata && file_metadata[:posix]
1159
+ uid = Tpkg::lookup_uid(file_metadata[:posix][:owner]) if file_metadata[:posix][:owner]
1160
+ gid = Tpkg::lookup_uid(file_metadata[:posix][:group]) if file_metadata[:posix][:group]
1161
+ perms = file_metadata[:posix][:perms] if file_metadata[:posix][:perms]
1162
+ end
1163
+ return perms, uid, gid
1164
+ end
1063
1165
 
1064
1166
  #
1065
1167
  # Instance methods
@@ -1598,7 +1700,7 @@ class Tpkg
1598
1700
  metadata = {}
1599
1701
  if File.directory?(@installed_directory)
1600
1702
  Dir.foreach(@installed_directory) do |entry|
1601
- next if entry == '.' || entry == '..' || entry == 'metadata'
1703
+ next if entry == '.' || entry == '..' || entry == 'metadata' || !Tpkg::valid_pkg_filename?(entry)
1602
1704
  # Check the timestamp on the file to see if it is new or has
1603
1705
  # changed since we last loaded data
1604
1706
  timestamp = File.mtime(File.join(@installed_directory, entry))
@@ -1642,7 +1744,7 @@ class Tpkg
1642
1744
  end
1643
1745
  end
1644
1746
  metadata[entry] = { :timestamp => timestamp,
1645
- :metadata => m }
1747
+ :metadata => m } unless m.nil?
1646
1748
  end
1647
1749
  end
1648
1750
  end
@@ -1652,13 +1754,15 @@ class Tpkg
1652
1754
  end
1653
1755
 
1654
1756
  # Convert metadata_for_installed_packages into pkg hashes
1655
- def installed_packages
1757
+ def installed_packages(pkgname=nil)
1656
1758
  instpkgs = []
1657
1759
  metadata_for_installed_packages.each do |metadata|
1658
- instpkgs << { :metadata => metadata,
1659
- :source => :currently_installed,
1660
- # It seems reasonable for this to default to true
1661
- :prefer => true }
1760
+ if !pkgname || metadata[:name] == pkgname
1761
+ instpkgs << { :metadata => metadata,
1762
+ :source => :currently_installed,
1763
+ # It seems reasonable for this to default to true
1764
+ :prefer => true }
1765
+ end
1662
1766
  end
1663
1767
  instpkgs
1664
1768
  end
@@ -1740,7 +1844,29 @@ class Tpkg
1740
1844
  end
1741
1845
  end
1742
1846
  else
1743
- installed_packages.each do |pkg|
1847
+ pkgname = nil
1848
+ if req && req[:name]
1849
+ pkgname = req[:name]
1850
+ end
1851
+ # Passing a package name if we have one to installed_packages serves
1852
+ # primarily to make following the debugging output of dependency
1853
+ # resolution easier. The dependency resolution process makes frequent
1854
+ # calls to available_packages_that_meet_requirement, which in turn calls
1855
+ # this method. For available packages we're able to pre-filter based on
1856
+ # package name before calling package_meets_requirement? because we
1857
+ # store available packages hashed based on package name.
1858
+ # package_meets_requirement? is fairly verbose in its debugging output,
1859
+ # so the user sees each package it checks against a given requirement.
1860
+ # It is therefore a bit disconcerting when trying to follow the
1861
+ # debugging output to see the fairly clean process of checking available
1862
+ # packages which have already been filtered to match the desired name,
1863
+ # and then available_packages_that_meet_requirement calls this method,
1864
+ # and the user starts to see every installed package checked against the
1865
+ # same requirement. It is not obvious to the someone why all of a
1866
+ # sudden packages that aren't even remotely close to the requirement
1867
+ # start getting checked. Doing a pre-filter based on package name here
1868
+ # makes the process more consistent and easier to follow.
1869
+ installed_packages(pkgname).each do |pkg|
1744
1870
  if req
1745
1871
  if Tpkg::package_meets_requirement?(pkg, req)
1746
1872
  pkgs << pkg
@@ -1796,9 +1922,7 @@ class Tpkg
1796
1922
  # will be an array of package specs.
1797
1923
  MAX_POSSIBLE_SOLUTIONS_TO_CHECK = 10000
1798
1924
  def best_solution(requirements, packages, core_packages)
1799
- # Dup objects passed to us so that resolve_dependencies is free to
1800
- # change them without potentially messing up our caller
1801
- result = resolve_dependencies(requirements.dup, {:tpkg => packages.dup, :native => {}}, core_packages.dup)
1925
+ result = resolve_dependencies(requirements, {:tpkg => packages, :native => {}}, core_packages)
1802
1926
  if @@debug
1803
1927
  if result[:solution]
1804
1928
  puts "bestsol picks: #{result[:solution].inspect}" if @@debug
@@ -1820,6 +1944,10 @@ class Tpkg
1820
1944
  # the same name. This may be necessary if different dependencies of the
1821
1945
  # core packages end up needing both.
1822
1946
  def resolve_dependencies(requirements, packages, core_packages, number_of_possible_solutions_checked=0)
1947
+ # We're probably going to make changes to packages, dup it now so
1948
+ # that we don't mess up the caller's state.
1949
+ packages = {:tpkg => packages[:tpkg].dup, :native => packages[:native].dup}
1950
+
1823
1951
  # Make sure we have populated package lists for all requirements.
1824
1952
  # Filter the package lists against the requirements and
1825
1953
  # ensure we can at least satisfy the initial requirements.
@@ -1849,6 +1977,13 @@ class Tpkg
1849
1977
  return {:number_of_possible_solutions_checked => number_of_possible_solutions_checked}
1850
1978
  end
1851
1979
  end
1980
+
1981
+ # FIXME: Should we weed out any entries in packages that don't correspond
1982
+ # to something in requirements? We operate later on the assumption that
1983
+ # there are no such entries. Because we dup packages at the right points
1984
+ # I believe we'll never accidently end up with orphaned entries, but maybe
1985
+ # it would be worth the compute cycles to make sure?
1986
+
1852
1987
  # Sort the packages
1853
1988
  [:tpkg, :native].each do |type|
1854
1989
  packages[type].each do |pkgname, pkgs|
@@ -1857,7 +1992,8 @@ class Tpkg
1857
1992
  # Anything else can score 1 at best. This ensures
1858
1993
  # that we prefer the solution which leaves the most
1859
1994
  # currently installed packages alone.
1860
- if pkgs[0][:source] != :currently_installed &&
1995
+ if pkgs[0] &&
1996
+ pkgs[0][:source] != :currently_installed &&
1861
1997
  pkgs[0][:source] != :native_installed
1862
1998
  pkgs.unshift(nil)
1863
1999
  end
@@ -2001,6 +2137,7 @@ class Tpkg
2001
2137
  if sol[:remaining_noncoredepth] == 0
2002
2138
  result = check_solution(sol, requirements, packages, core_packages, number_of_possible_solutions_checked)
2003
2139
  if result[:solution]
2140
+ puts "resolvdeps returning successful solution" if @@debug
2004
2141
  return result
2005
2142
  else
2006
2143
  number_of_possible_solutions_checked = result[:number_of_possible_solutions_checked]
@@ -2024,6 +2161,7 @@ class Tpkg
2024
2161
  end
2025
2162
  end
2026
2163
  # No solutions found
2164
+ puts "resolvedeps returning failure" if @@debug
2027
2165
  return {:number_of_possible_solutions_checked => number_of_possible_solutions_checked}
2028
2166
  end
2029
2167
 
@@ -2036,7 +2174,7 @@ class Tpkg
2036
2174
  end
2037
2175
 
2038
2176
  if @@debug
2039
- puts "checksol checking #{solution.inspect}"
2177
+ puts "checksol checking sol #{solution.inspect}"
2040
2178
  end
2041
2179
 
2042
2180
  # Extract dependencies from each package in the solution
@@ -2064,7 +2202,7 @@ class Tpkg
2064
2202
  if packages[newreq[:type]][newreq[:name]]
2065
2203
  pkg = solution[:pkgs].find{|solpkg| solpkg[:metadata][:name] == newreq[:name]}
2066
2204
  puts "checksol newreq pkg: #{pkg.inspect}" if @@debug
2067
- if Tpkg::package_meets_requirement?(pkg, newreq)
2205
+ if pkg && Tpkg::package_meets_requirement?(pkg, newreq)
2068
2206
  # No change to solution needed
2069
2207
  else
2070
2208
  # Solution no longer works
@@ -2082,14 +2220,16 @@ class Tpkg
2082
2220
  return {:solution => solution[:pkgs]}
2083
2221
  else
2084
2222
  puts "checksol newreqs need packages, calling resolvedeps" if @@debug
2085
- result = resolve_dependencies(requirements+newreqs_that_need_packages, packages.dup, core_packages, number_of_possible_solutions_checked)
2223
+ result = resolve_dependencies(requirements+newreqs_that_need_packages, packages, core_packages, number_of_possible_solutions_checked)
2086
2224
  if result[:solution]
2225
+ puts "checksol returning successful solution" if @@debug
2087
2226
  return result
2088
2227
  else
2089
2228
  number_of_possible_solutions_checked = result[:number_of_possible_solutions_checked]
2090
2229
  end
2091
2230
  end
2092
2231
  end
2232
+ puts "checksol returning failure" if @@debug
2093
2233
  return {:number_of_possible_solutions_checked => number_of_possible_solutions_checked}
2094
2234
  end
2095
2235
 
@@ -2320,7 +2460,7 @@ class Tpkg
2320
2460
  def unpack(package_file, passphrase=nil, options={})
2321
2461
  ret_val = 0
2322
2462
  metadata = Tpkg::metadata_from_package(package_file)
2323
-
2463
+
2324
2464
  # Unpack files in a temporary directory
2325
2465
  # I'd prefer to unpack on the fly so that the user doesn't need to
2326
2466
  # have disk space to hold three copies of the package (the package
@@ -2330,7 +2470,7 @@ class Tpkg
2330
2470
  # directory structure in the package.
2331
2471
  topleveldir = Tpkg::package_toplevel_directory(package_file)
2332
2472
  workdir = Tpkg::tempdir(topleveldir, @tmp_directory)
2333
- system("#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{@tar} -C #{workdir} -xpf -")
2473
+ system("#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} #{@@taroptions} | #{@tar} #{@@taroptions} -C #{workdir} -xpf -")
2334
2474
  files_info = {} # store perms, uid, gid, etc. for files
2335
2475
  checksums_of_decrypted_files = {}
2336
2476
  root_dir = File.join(workdir, 'tpkg', 'root')
@@ -2341,7 +2481,7 @@ class Tpkg
2341
2481
  # Get list of conflicting files/directories & store their perm/ownership. That way, we can
2342
2482
  # set them to the correct values later on in order to preserve them.
2343
2483
  # TODO: verify this command works on all platforms
2344
- files = `#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{@tar} -tf -`
2484
+ files = `#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} #{@@taroptions} | #{@tar} #{@@taroptions} -tf -`
2345
2485
  files = files.split("\n")
2346
2486
  conflicting_files = {}
2347
2487
  files.each do | file |
@@ -2590,119 +2730,8 @@ class Tpkg
2590
2730
  system("#{@tar} -C #{File.join(workdir, 'tpkg', 'reloc')} -cf - . | #{@tar} -C #{@base} -xpf -")
2591
2731
  end
2592
2732
 
2593
- # Install any init scripts
2594
- init_links(metadata).each do |link, init_script|
2595
- # We don't have to any anything if there's already symlink to our init script.
2596
- # This can happen if user removes pkg manually without removing
2597
- # init symlink
2598
- next if File.symlink?(link) && File.readlink(link) == init_script
2599
- begin
2600
- if !File.exist?(File.dirname(link))
2601
- FileUtils.mkdir_p(File.dirname(link))
2602
- end
2603
- begin
2604
- File.symlink(init_script, link)
2605
- rescue Errno::EEXIST
2606
- # The link name that init_links provides is not guaranteed to
2607
- # be unique. It might collide with a base system init script
2608
- # or an init script from another tpkg. If the link name
2609
- # supplied by init_links results in EEXIST then try appending
2610
- # a number to the end of the link name.
2611
- catch :init_link_done do
2612
- 1.upto(9) do |i|
2613
- begin
2614
- File.symlink(init_script, link + i.to_s)
2615
- throw :init_link_done
2616
- rescue Errno::EEXIST
2617
- end
2618
- end
2619
- # If we get here (i.e. we never reached the throw) then we
2620
- # failed to create any of the possible link names.
2621
- raise "Failed to install init script #{init_script} -> #{link} for #{File.basename(package_file)}"
2622
- end
2623
- end
2624
- rescue Errno::EPERM
2625
- # If creating the link fails due to permission problems and
2626
- # we're not running as root just warn the user, allowing folks
2627
- # to run tpkg as a non-root user with reduced functionality.
2628
- if Process.euid == 0
2629
- raise
2630
- else
2631
- warn "Failed to install init script for #{File.basename(package_file)}, probably due to lack of root privileges"
2632
- end
2633
- end
2634
- end
2635
-
2636
- # Install any crontabs
2637
- crontab_destinations(metadata).each do |crontab, destination|
2638
- begin
2639
- if destination[:link]
2640
- next if File.symlink?(destination[:link]) && File.readlink(destination[:link]) == crontab
2641
- if !File.exist?(File.dirname(destination[:link]))
2642
- FileUtils.mkdir_p(File.dirname(destination[:link]))
2643
- end
2644
- begin
2645
- File.symlink(crontab, destination[:link])
2646
- rescue Errno::EEXIST
2647
- # The link name that crontab_destinations provides is not
2648
- # guaranteed to be unique. It might collide with a base
2649
- # system crontab or a crontab from another tpkg. If the
2650
- # link name supplied by crontab_destinations results in
2651
- # EEXIST then try appending a number to the end of the link
2652
- # name.
2653
- catch :crontab_link_done do
2654
- 1.upto(9) do |i|
2655
- begin
2656
- File.symlink(crontab, destination[:link] + i.to_s)
2657
- throw :crontab_link_done
2658
- rescue Errno::EEXIST
2659
- end
2660
- end
2661
- # If we get here (i.e. we never reached the throw) then we
2662
- # failed to create any of the possible link names.
2663
- raise "Failed to install crontab #{crontab} -> #{destination[:link]} for #{File.basename(package_file)}"
2664
- end
2665
- end
2666
- elsif destination[:file]
2667
- if !File.exist?(File.dirname(destination[:file]))
2668
- FileUtils.mkdir_p(File.dirname(destination[:file]))
2669
- end
2670
- tmpfile = Tempfile.new(File.basename(destination[:file]), File.dirname(destination[:file]))
2671
- if File.exist?(destination[:file])
2672
- # Match permissions and ownership of current crontab
2673
- st = File.stat(destination[:file])
2674
- File.chmod(st.mode & 07777, tmpfile.path)
2675
- File.chown(st.uid, st.gid, tmpfile.path)
2676
- # Insert the contents of the current crontab file
2677
- File.open(destination[:file]) { |file| tmpfile.write(file.read) }
2678
- end
2679
- # Insert a header line so we can find this section to remove later
2680
- tmpfile.puts "### TPKG START - #{@base} - #{File.basename(package_file)}"
2681
- # Insert the package crontab contents
2682
- crontab_contents = IO.read(crontab)
2683
- tmpfile.write(crontab_contents)
2684
- # Insert a newline if the crontab doesn't end with one
2685
- if crontab_contents.chomp == crontab_contents
2686
- tmpfile.puts
2687
- end
2688
- # Insert a footer line
2689
- tmpfile.puts "### TPKG END - #{@base} - #{File.basename(package_file)}"
2690
- tmpfile.close
2691
- File.rename(tmpfile.path, destination[:file])
2692
- # FIXME: On Solaris we should bounce cron or use the crontab
2693
- # command, otherwise cron won't pick up the changes
2694
- end
2695
- rescue Errno::EPERM
2696
- # If installing the crontab fails due to permission problems and
2697
- # we're not running as root just warn the user, allowing folks
2698
- # to run tpkg as a non-root user with reduced functionality.
2699
- if Process.euid == 0
2700
- raise
2701
- else
2702
- warn "Failed to install crontab for #{File.basename(package_file)}, probably due to lack of root privileges"
2703
- end
2704
- end
2705
- end
2733
+ install_init_scripts(metadata)
2734
+ install_crontabs(metadata)
2706
2735
 
2707
2736
  # Run postinstall script
2708
2737
  if File.exist?(File.join(workdir, 'tpkg', 'postinstall'))
@@ -2773,6 +2802,243 @@ class Tpkg
2773
2802
  return ret_val
2774
2803
  end
2775
2804
 
2805
+ def install_init_scripts(metadata)
2806
+ init_links(metadata).each do |link, init_script|
2807
+ # We don't have to do anything if there's already symlink to our init
2808
+ # script. This can happen if the user removes a package manually without
2809
+ # removing the init symlink
2810
+ next if File.symlink?(link) && File.readlink(link) == init_script
2811
+ begin
2812
+ if !File.exist?(File.dirname(link))
2813
+ FileUtils.mkdir_p(File.dirname(link))
2814
+ end
2815
+ begin
2816
+ File.symlink(init_script, link)
2817
+ rescue Errno::EEXIST
2818
+ # The link name that init_links provides is not guaranteed to
2819
+ # be unique. It might collide with a base system init script
2820
+ # or an init script from another tpkg. If the link name
2821
+ # supplied by init_links results in EEXIST then try appending
2822
+ # a number to the end of the link name.
2823
+ catch :init_link_done do
2824
+ 1.upto(9) do |i|
2825
+ begin
2826
+ File.symlink(init_script, link + i.to_s)
2827
+ throw :init_link_done
2828
+ rescue Errno::EEXIST
2829
+ end
2830
+ end
2831
+ # If we get here (i.e. we never reached the throw) then we
2832
+ # failed to create any of the possible link names.
2833
+ raise "Failed to install init script #{init_script} -> #{link} for #{File.basename(metadata[:filename].to_s)}, too many overlapping filenames"
2834
+ end
2835
+ end
2836
+ # EACCES for file/directory permissions issues
2837
+ rescue Errno::EACCES => e
2838
+ # If creating the link fails due to permission problems and
2839
+ # we're not running as root just warn the user, allowing folks
2840
+ # to run tpkg as a non-root user with reduced functionality.
2841
+ if Process.euid != 0
2842
+ warn "Failed to install init script for #{File.basename(metadata[:filename].to_s)}, probably due to lack of root privileges: #{e.message}"
2843
+ else
2844
+ raise e
2845
+ end
2846
+ end
2847
+ end
2848
+ end
2849
+ def remove_init_scripts(metadata)
2850
+ init_links(metadata).each do |link, init_script|
2851
+ # The link we ended up making when we unpacked the package could be any
2852
+ # of a series (see the code in install_init_scripts for the reasoning),
2853
+ # we need to check them all.
2854
+ links = [link]
2855
+ links.concat((1..9).to_a.map { |i| link + i.to_s })
2856
+ links.each do |l|
2857
+ if File.symlink?(l) && File.readlink(l) == init_script
2858
+ begin
2859
+ File.delete(l)
2860
+ # EACCES for file/directory permissions issues
2861
+ rescue Errno::EACCES => e
2862
+ # If removing the link fails due to permission problems and
2863
+ # we're not running as root just warn the user, allowing folks
2864
+ # to run tpkg as a non-root user with reduced functionality.
2865
+ if Process.euid != 0
2866
+ warn "Failed to remove init script for #{File.basename(metadata[:filename].to_s)}, probably due to lack of root privileges: #{e.message}"
2867
+ else
2868
+ raise e
2869
+ end
2870
+ end
2871
+ end
2872
+ end
2873
+ end
2874
+ end
2875
+
2876
+ def install_crontabs(metadata)
2877
+ crontab_destinations(metadata).each do |crontab, destination|
2878
+ begin
2879
+ if destination[:link]
2880
+ install_crontab_link(metadata, crontab, destination)
2881
+ elsif destination[:file]
2882
+ install_crontab_file(metadata, crontab, destination)
2883
+ end
2884
+ # EACCES for file/directory permissions issues
2885
+ rescue Errno::EACCES => e
2886
+ # If installing the crontab fails due to permission problems and
2887
+ # we're not running as root just warn the user, allowing folks
2888
+ # to run tpkg as a non-root user with reduced functionality.
2889
+ if Process.euid != 0
2890
+ warn "Failed to install crontab for #{File.basename(metadata[:filename].to_s)}, probably due to lack of root privileges: #{e.message}"
2891
+ else
2892
+ raise e
2893
+ end
2894
+ rescue RuntimeError => e
2895
+ if e.message.include?('cannot generate tempfile') && Process.euid != 0
2896
+ warn "Failed to install crontab for #{File.basename(metadata[:filename].to_s)}, probably due to lack of root privileges: #{e.message}"
2897
+ else
2898
+ raise e
2899
+ end
2900
+ end
2901
+ end
2902
+ end
2903
+ def install_crontab_link(metadata, crontab, destination)
2904
+ return if File.symlink?(destination[:link]) && File.readlink(destination[:link]) == crontab
2905
+ if !File.exist?(File.dirname(destination[:link]))
2906
+ FileUtils.mkdir_p(File.dirname(destination[:link]))
2907
+ end
2908
+ begin
2909
+ File.symlink(crontab, destination[:link])
2910
+ rescue Errno::EEXIST
2911
+ # The link name that crontab_destinations provides is not
2912
+ # guaranteed to be unique. It might collide with a base
2913
+ # system crontab or a crontab from another tpkg. If the
2914
+ # link name supplied by crontab_destinations results in
2915
+ # EEXIST then try appending a number to the end of the link
2916
+ # name.
2917
+ catch :crontab_link_done do
2918
+ 1.upto(9) do |i|
2919
+ begin
2920
+ File.symlink(crontab, destination[:link] + i.to_s)
2921
+ throw :crontab_link_done
2922
+ rescue Errno::EEXIST
2923
+ end
2924
+ end
2925
+ # If we get here (i.e. we never reached the throw) then we
2926
+ # failed to create any of the possible link names.
2927
+ raise "Failed to install crontab #{crontab} -> #{destination[:link]} for #{File.basename(metadata[:filename].to_s)}, too many overlapping filenames"
2928
+ end
2929
+ end
2930
+ end
2931
+ def install_crontab_file(metadata, crontab, destination)
2932
+ if !File.exist?(File.dirname(destination[:file]))
2933
+ FileUtils.mkdir_p(File.dirname(destination[:file]))
2934
+ end
2935
+ tmpfile = Tempfile.new(File.basename(destination[:file]), File.dirname(destination[:file]))
2936
+ if File.exist?(destination[:file])
2937
+ # Match permissions and ownership of current crontab
2938
+ st = File.stat(destination[:file])
2939
+ begin
2940
+ File.chmod(st.mode & 07777, tmpfile.path)
2941
+ File.chown(st.uid, st.gid, tmpfile.path)
2942
+ # EPERM for attempts to chown/chmod as non-root user
2943
+ rescue Errno::EPERM => e
2944
+ # If installing the crontab fails due to permission problems and
2945
+ # we're not running as root just warn the user, allowing folks
2946
+ # to run tpkg as a non-root user with reduced functionality.
2947
+ if Process.euid != 0
2948
+ warn "Failed to install crontab for #{File.basename(metadata[:filename].to_s)}, probably due to lack of root privileges: #{e.message}"
2949
+ else
2950
+ raise e
2951
+ end
2952
+ end
2953
+ # Insert the contents of the current crontab file
2954
+ File.open(destination[:file]) { |file| tmpfile.write(file.read) }
2955
+ end
2956
+ # Insert a header line so we can find this section to remove later
2957
+ tmpfile.puts "### TPKG START - #{@base} - #{File.basename(metadata[:filename].to_s)}"
2958
+ # Insert the package crontab contents
2959
+ crontab_contents = IO.read(crontab)
2960
+ tmpfile.write(crontab_contents)
2961
+ # Insert a newline if the crontab doesn't end with one
2962
+ if crontab_contents.chomp == crontab_contents
2963
+ tmpfile.puts
2964
+ end
2965
+ # Insert a footer line
2966
+ tmpfile.puts "### TPKG END - #{@base} - #{File.basename(metadata[:filename].to_s)}"
2967
+ tmpfile.close
2968
+ File.rename(tmpfile.path, destination[:file])
2969
+ # FIXME: On Solaris we should bounce cron or use the crontab
2970
+ # command, otherwise cron won't pick up the changes
2971
+ end
2972
+ def remove_crontabs(metadata)
2973
+ crontab_destinations(metadata).each do |crontab, destination|
2974
+ begin
2975
+ if destination[:link]
2976
+ remove_crontab_link(metadata, crontab, destination)
2977
+ elsif destination[:file]
2978
+ remove_crontab_file(metadata, crontab, destination)
2979
+ end
2980
+ # EACCES for file/directory permissions issues
2981
+ rescue Errno::EACCES => e
2982
+ # If removing the crontab fails due to permission problems and
2983
+ # we're not running as root just warn the user, allowing folks
2984
+ # to run tpkg as a non-root user with reduced functionality.
2985
+ if Process.euid != 0
2986
+ warn "Failed to remove crontab for #{File.basename(metadata[:filename].to_s)}, probably due to lack of root privileges: #{e.message}"
2987
+ else
2988
+ raise e
2989
+ end
2990
+ end
2991
+ end
2992
+ end
2993
+ def remove_crontab_link(metadata, crontab, destination)
2994
+ # The link we ended up making when we unpacked the package could
2995
+ # be any of a series (see the code in unpack for the reasoning),
2996
+ # we need to check them all.
2997
+ links = [destination[:link]]
2998
+ links.concat((1..9).to_a.map { |i| destination[:link] + i.to_s })
2999
+ links.each do |l|
3000
+ if File.symlink?(l) && File.readlink(l) == crontab
3001
+ File.delete(l)
3002
+ end
3003
+ end
3004
+ end
3005
+ def remove_crontab_file(metadata, crontab, destination)
3006
+ if File.exist?(destination[:file])
3007
+ tmpfile = Tempfile.new(File.basename(destination[:file]), File.dirname(destination[:file]))
3008
+ # Match permissions and ownership of current crontab
3009
+ st = File.stat(destination[:file])
3010
+ begin
3011
+ File.chmod(st.mode & 07777, tmpfile.path)
3012
+ File.chown(st.uid, st.gid, tmpfile.path)
3013
+ # EPERM for attempts to chown/chmod as non-root user
3014
+ rescue Errno::EPERM => e
3015
+ # If installing the crontab fails due to permission problems and
3016
+ # we're not running as root just warn the user, allowing folks
3017
+ # to run tpkg as a non-root user with reduced functionality.
3018
+ if Process.euid != 0
3019
+ warn "Failed to install crontab for #{File.basename(metadata[:filename].to_s)}, probably due to lack of root privileges: #{e.message}"
3020
+ else
3021
+ raise
3022
+ end
3023
+ end
3024
+ # Remove section associated with this package
3025
+ skip = false
3026
+ IO.foreach(destination[:file]) do |line|
3027
+ if line == "### TPKG START - #{@base} - #{File.basename(metadata[:filename].to_s)}\n"
3028
+ skip = true
3029
+ elsif line == "### TPKG END - #{@base} - #{File.basename(metadata[:filename].to_s)}\n"
3030
+ skip = false
3031
+ elsif !skip
3032
+ tmpfile.write(line)
3033
+ end
3034
+ end
3035
+ tmpfile.close
3036
+ File.rename(tmpfile.path, destination[:file])
3037
+ # FIXME: On Solaris we should bounce cron or use the crontab
3038
+ # command, otherwise cron won't pick up the changes
3039
+ end
3040
+ end
3041
+
2776
3042
  def requirements_for_currently_installed_package(pkgname=nil)
2777
3043
  requirements = []
2778
3044
  metadata_for_installed_packages.each do |metadata|
@@ -2833,7 +3099,7 @@ class Tpkg
2833
3099
  req = Tpkg::parse_request(request, @installed_directory)
2834
3100
  newreqs << req
2835
3101
 
2836
- # Initialize the list of possible packages for this req
3102
+ puts "Initializing the list of possible packages for this req" if @@debug
2837
3103
  if !packages[req[:name]]
2838
3104
  packages[req[:name]] = available_packages_that_meet_requirement(req)
2839
3105
  end
@@ -2843,7 +3109,14 @@ class Tpkg
2843
3109
  source = nil
2844
3110
  localpath = nil
2845
3111
  if File.file?(request)
3112
+ raise "Invalid package filename #{request}" if !Tpkg::valid_pkg_filename?(request)
3113
+
2846
3114
  puts "parse_requests treating request as a file" if @@debug
3115
+
3116
+ if request !~ /\.tpkg$/
3117
+ warn "Warning: Attempting to perform the request on #{File.expand_path(request)}. This might not be a valid package file."
3118
+ end
3119
+
2847
3120
  localpath = request
2848
3121
  metadata = Tpkg::metadata_from_package(request)
2849
3122
  source = request
@@ -2892,7 +3165,8 @@ class Tpkg
2892
3165
  good_package = true
2893
3166
  metadata = pkg[:metadata]
2894
3167
  req = { :name => metadata[:name], :type => :tpkg }
2895
- # Quick sanity check that the package can be installed on this machine.
3168
+ # Quick sanity check that the package can be installed on this machine.
3169
+ puts "check_requests checking that available package for request works on this machine: #{pkg.inspect}" if @@debug
2896
3170
  if !Tpkg::package_meets_requirement?(pkg, req)
2897
3171
  possible_errors << " Requested package #{metadata[:filename]} doesn't match this machine's OS or architecture"
2898
3172
  good_package = false
@@ -2901,6 +3175,7 @@ class Tpkg
2901
3175
  # a sanity check that there is at least one package
2902
3176
  # available for each dependency of this package
2903
3177
  metadata[:dependencies].each do |depreq|
3178
+ puts "check_requests checking for available packages to satisfy dependency: #{depreq.inspect}" if @@debug
2904
3179
  if available_packages_that_meet_requirement(depreq).empty? && !Tpkg::packages_meet_requirement?(packages.values.flatten, depreq)
2905
3180
  possible_errors << " Requested package #{metadata[:filename]} depends on #{depreq.inspect}, no packages that satisfy that dependency are available"
2906
3181
  good_package = false
@@ -3386,6 +3661,11 @@ class Tpkg
3386
3661
  else
3387
3662
  pkgfile = download(pkg[:source], pkg[:metadata][:filename])
3388
3663
  end
3664
+
3665
+ if !Tpkg::valid_pkg_filename?(pkgfile)
3666
+ raise "Invalid package filename: #{pkgfile}"
3667
+ end
3668
+
3389
3669
  if prompt_for_conflicting_files(pkgfile, CHECK_UPGRADE)
3390
3670
  # If the old and new packages have overlapping externals flag them
3391
3671
  # to be skipped so that the external isn't removed and then
@@ -3399,7 +3679,7 @@ class Tpkg
3399
3679
  end if pkg[:metadata][:externals]
3400
3680
 
3401
3681
  # Remove the old package if we haven't done so
3402
- unless removed_pkgs.include?(pkg[:metadata][:name])
3682
+ unless oldpkgs.nil? or oldpkgs.empty? or removed_pkgs.include?(pkg[:metadata][:name])
3403
3683
  remove([pkg[:metadata][:name]], :upgrade => true, :externals_to_skip => externals_to_skip)
3404
3684
  removed_pkgs << pkg[:metadata][:name]
3405
3685
  end
@@ -3441,6 +3721,7 @@ class Tpkg
3441
3721
 
3442
3722
  packages_to_remove = nil
3443
3723
  if requests
3724
+ requests.uniq! if requests.is_a?(Array)
3444
3725
  packages_to_remove = []
3445
3726
  requests.each do |request|
3446
3727
  req = Tpkg::parse_request(request, @installed_directory)
@@ -3548,7 +3829,7 @@ class Tpkg
3548
3829
 
3549
3830
  topleveldir = Tpkg::package_toplevel_directory(package_file)
3550
3831
  workdir = Tpkg::tempdir(topleveldir, @tmp_directory)
3551
- system("#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} | #{@tar} -C #{workdir} -xpf -")
3832
+ system("#{@tar} -xf #{package_file} -O #{File.join(topleveldir, 'tpkg.tar')} #{@@taroptions} | #{@tar} #{@@taroptions} -C #{workdir} -xpf -")
3552
3833
 
3553
3834
  # Run preremove script
3554
3835
  if File.exist?(File.join(workdir, 'tpkg', 'preremove'))
@@ -3571,86 +3852,9 @@ class Tpkg
3571
3852
  # Switch back to our previous directory
3572
3853
  Dir.chdir(pwd)
3573
3854
  end
3574
-
3575
- # Remove any init scripts
3576
- init_links(pkg[:metadata]).each do |link, init_script|
3577
- # The link we ended up making when we unpacked the package could
3578
- # be any of a series (see the code in unpack for the reasoning),
3579
- # we need to check them all.
3580
- links = [link]
3581
- links.concat((1..9).to_a.map { |i| link + i.to_s })
3582
- links.each do |l|
3583
- if File.symlink?(l) && File.readlink(l) == init_script
3584
- begin
3585
- File.delete(l)
3586
- rescue Errno::EPERM
3587
- if Process.euid == 0
3588
- raise
3589
- else
3590
- warn "Failed to remove init script for #{File.basename(package_file)}, probably due to lack of root privileges"
3591
- end
3592
- end
3593
- end
3594
- end
3595
- end
3596
-
3597
- # Remove any crontabs
3598
- crontab_destinations(pkg[:metadata]).each do |crontab, destination|
3599
- begin
3600
- if destination[:link]
3601
- # The link we ended up making when we unpacked the package could
3602
- # be any of a series (see the code in unpack for the reasoning),
3603
- # we need to check them all.
3604
- links = [destination[:link]]
3605
- links.concat((1..9).to_a.map { |i| destination[:link] + i.to_s })
3606
- links.each do |l|
3607
- if File.symlink?(l) && File.readlink(l) == crontab
3608
- begin
3609
- File.delete(l)
3610
- rescue Errno::EPERM
3611
- if Process.euid == 0
3612
- raise
3613
- else
3614
- warn "Failed to remove crontab for #{File.basename(package_file)}, probably due to lack of root privileges"
3615
- end
3616
- end
3617
- end
3618
- end
3619
- elsif destination[:file]
3620
- if File.exist?(destination[:file])
3621
- tmpfile = Tempfile.new(File.basename(destination[:file]), File.dirname(destination[:file]))
3622
- # Match permissions and ownership of current crontab
3623
- st = File.stat(destination[:file])
3624
- File.chmod(st.mode & 07777, tmpfile.path)
3625
- File.chown(st.uid, st.gid, tmpfile.path)
3626
- # Remove section associated with this package
3627
- skip = false
3628
- IO.foreach(destination[:file]) do |line|
3629
- if line == "### TPKG START - #{@base} - #{File.basename(package_file)}\n"
3630
- skip = true
3631
- elsif line == "### TPKG END - #{@base} - #{File.basename(package_file)}\n"
3632
- skip = false
3633
- elsif !skip
3634
- tmpfile.write(line)
3635
- end
3636
- end
3637
- tmpfile.close
3638
- File.rename(tmpfile.path, destination[:file])
3639
- # FIXME: On Solaris we should bounce cron or use the crontab
3640
- # command, otherwise cron won't pick up the changes
3641
- end
3642
- end
3643
- rescue Errno::EPERM
3644
- # If removing the crontab fails due to permission problems and
3645
- # we're not running as root just warn the user, allowing folks
3646
- # to run tpkg as a non-root user with reduced functionality.
3647
- if Process.euid == 0
3648
- raise
3649
- else
3650
- warn "Failed to remove crontab for #{File.basename(package_file)}, probably due to lack of root privileges"
3651
- end
3652
- end
3653
- end
3855
+
3856
+ remove_init_scripts(pkg[:metadata])
3857
+ remove_crontabs(pkg[:metadata])
3654
3858
 
3655
3859
  # Run any externals
3656
3860
  pkg[:metadata][:externals].each do |external|
@@ -3678,6 +3882,12 @@ class Tpkg
3678
3882
  end
3679
3883
  rescue Errno::ENOENT
3680
3884
  warn "File #{file} from package #{File.basename(package_file)} missing during remove"
3885
+ # I know it's bad to have a generic rescue for all exceptions, but in this case, there
3886
+ # can be many things that might go wrong when removing a file. We don't want tpkg
3887
+ # to crash and leave the packages in a bad state. It's better to catch
3888
+ # all exceptions and give the user some warnings.
3889
+ rescue
3890
+ warn "Failed to remove file #{file}."
3681
3891
  end
3682
3892
  end
3683
3893
 
@@ -3845,6 +4055,7 @@ class Tpkg
3845
4055
  if requested_packages.nil?
3846
4056
  packages_to_execute_on = installed_packages_that_meet_requirement(nil)
3847
4057
  else
4058
+ requested_packages.uniq!
3848
4059
  requested_packages.each do |request|
3849
4060
  req = Tpkg::parse_request(request)
3850
4061
  packages_to_execute_on.concat(installed_packages_that_meet_requirement(req))
@@ -3958,21 +4169,25 @@ class Tpkg
3958
4169
  end
3959
4170
  end
3960
4171
 
3961
- # TODO: update server side to accept yaml data
3962
4172
  def send_update_to_server
3963
4173
  metadata = metadata_for_installed_packages.collect{|metadata| metadata.to_hash}
3964
4174
  yml = YAML.dump(metadata)
3965
4175
  begin
3966
- update_uri = URI.parse("#{@report_server}")
3967
- http = Tpkg::gethttp(update_uri)
3968
- request = {"yml"=>URI.escape(yml), "client"=>Facter['fqdn'].value}
3969
- post = Net::HTTP::Post.new(update_uri.path)
3970
- post.set_form_data(request)
3971
- response = http.request(post)
4176
+ response = nil
4177
+ # Need to set timeout otherwise tpkg can hang for a long time when having
4178
+ # problem talking to the reporter server.
4179
+ # I can't seem get net-ssh timeout to work so we'll just handle the timeout ourselves
4180
+ timeout(CONNECTION_TIMEOUT) do
4181
+ update_uri = URI.parse("#{@report_server}")
4182
+ http = Tpkg::gethttp(update_uri)
4183
+ request = {"yml"=>URI.escape(yml), "client"=>Facter['fqdn'].value}
4184
+ post = Net::HTTP::Post.new(update_uri.path)
4185
+ post.set_form_data(request)
4186
+ response = http.request(post)
4187
+ end
3972
4188
 
3973
4189
  case response
3974
4190
  when Net::HTTPSuccess
3975
- # puts "Response from server:\n'#{response.body}'"
3976
4191
  puts "Successfully send update to reporter server"
3977
4192
  else
3978
4193
  $stderr.puts response.body
@@ -3980,10 +4195,12 @@ class Tpkg
3980
4195
  # just ignore error and give user warning
3981
4196
  puts "Failed to send update to reporter server"
3982
4197
  end
4198
+ rescue Timeout::Error
4199
+ puts "Timed out when trying to send update to reporter server"
3983
4200
  rescue
3984
4201
  puts "Failed to send update to reporter server"
3985
4202
  end
3986
- end
4203
+ end
3987
4204
 
3988
4205
  # Build a dependency map of currently installed packages
3989
4206
  # For example, if we have pkgB and pkgC which depends on pkgA, then