tpkg 1.18.2 → 1.19.2

Sign up to get free protection for your applications and to get access to all the features.
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