fpm 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -128,7 +128,7 @@ class FPM::Package::Dir < FPM::Package
128
128
  "to stage files during packaging, so this setting would have " \
129
129
  "caused fpm to loop creating staging directories and copying " \
130
130
  "them into your package! Oops! If you are confused, maybe you could " \
131
- "check your TMPDIR or TEMPDIR environment variables?"
131
+ "check your TMPDIR, TMP, or TEMP environment variables?"
132
132
  end
133
133
 
134
134
  # For single file copies, permit file destinations
@@ -160,7 +160,7 @@ class FPM::Package::Dir < FPM::Package
160
160
  # lstat to follow symlinks
161
161
  dstat = File.stat(directory) rescue nil
162
162
  if dstat.nil?
163
- FileUtils.mkdir_p(directory)
163
+ FileUtils.mkdir_p(directory, :mode => 0755)
164
164
  elsif dstat.directory?
165
165
  # do nothing, it's already a directory!
166
166
  else
@@ -0,0 +1,165 @@
1
+ require "backports" # gem backports
2
+ require "fpm/package"
3
+ require "fpm/util"
4
+ require "digest"
5
+ require "fileutils"
6
+ require "rubygems/package"
7
+ require "xz"
8
+ require "corefines"
9
+
10
+ class FPM::Package::FreeBSD < FPM::Package
11
+ SCRIPT_MAP = {
12
+ :before_install => "pre-install",
13
+ :after_install => "post-install",
14
+ :before_remove => "pre-deinstall",
15
+ :after_remove => "post-deinstall",
16
+ } unless defined?(SCRIPT_MAP)
17
+
18
+ def self.default_abi
19
+ abi_name = %x{uname -s}.chomp
20
+ abi_version = %x{uname -r}.chomp.split(".")[0]
21
+ abi_arch = %x{uname -m}.chomp
22
+
23
+ [abi_name, abi_version, abi_arch].join(":")
24
+ end
25
+
26
+ option "--abi", "ABI",
27
+ "Sets the FreeBSD abi pkg field to specify binary compatibility.",
28
+ :default => default_abi
29
+
30
+ option "--origin", "ABI",
31
+ "Sets the FreeBSD 'origin' pkg field",
32
+ :default => "fpm/<name>"
33
+
34
+ def output(output_path)
35
+ output_check(output_path)
36
+
37
+ # Build the packaging metadata files.
38
+ checksums = {}
39
+ self.files.each do |f|
40
+ path = staging_path(f)
41
+ if File.symlink?(path)
42
+ checksums[f] = "-"
43
+ elsif File.file?(path)
44
+ checksums[f] = Digest::SHA256.file(path).hexdigest
45
+ end
46
+ end
47
+
48
+ pkg_origin = attributes[:freebsd_origin]
49
+ if pkg_origin == "fpm/<name>" # fill in default
50
+ pkg_origin = "fpm/#{name}"
51
+ end
52
+
53
+ pkg_version = "#{version}-#{iteration || 1}"
54
+
55
+ pkgdata = {
56
+ "abi" => attributes[:freebsd_abi],
57
+ "name" => name,
58
+ "version" => pkg_version,
59
+ "comment" => description,
60
+ "desc" => description,
61
+ "origin" => pkg_origin,
62
+ "maintainer" => maintainer,
63
+ "www" => url,
64
+ # prefix is required, but it doesn't seem to matter
65
+ "prefix" => "/",
66
+ }
67
+
68
+ # Write +COMPACT_MANIFEST, without the "files" section.
69
+ File.open(staging_path("+COMPACT_MANIFEST"), "w+") do |file|
70
+ file.write(pkgdata.to_json + "\n")
71
+ end
72
+
73
+ # Populate files + checksums, then write +MANIFEST.
74
+ pkgdata["files"] = {}
75
+ checksums.each do |f, shasum|
76
+ # pkg expands % URL-style escapes, so make sure to escape % as %25
77
+ pkgdata["files"]["/" + f.gsub("%", "%25")] = shasum
78
+ end
79
+
80
+ # Populate scripts
81
+ pkgdata["scripts"] = {}
82
+ scripts.each do |name, data|
83
+ pkgdata["scripts"][SCRIPT_MAP[name]] = data
84
+ end
85
+
86
+ File.open(staging_path("+MANIFEST"), "w+") do |file|
87
+ file.write(pkgdata.to_json + "\n")
88
+ end
89
+
90
+ # Create the .txz package archive from the files in staging_path.
91
+ File.open(output_path, "wb") do |file|
92
+ XZ::StreamWriter.new(file) do |xz|
93
+ ::Gem::Package::TarWriter.new(xz) do |tar|
94
+ # The manifests must come first for pkg.
95
+ add_path(tar, "+COMPACT_MANIFEST",
96
+ File.join(staging_path, "+COMPACT_MANIFEST"))
97
+ add_path(tar, "+MANIFEST",
98
+ File.join(staging_path, "+MANIFEST"))
99
+
100
+ checksums.keys.each do |path|
101
+ add_path(tar, "/" + path, File.join(staging_path, path))
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end # def output
107
+
108
+ def add_path(tar, tar_path, path)
109
+ stat = File.lstat(path)
110
+ if stat.directory?
111
+ tar.mkdir(tar_path, stat.mode)
112
+ elsif stat.symlink?
113
+ tar.add_symlink(tar_path, File.readlink(path), stat.mode)
114
+ else
115
+ tar.add_file_simple(tar_path, stat.mode, stat.size) do |io|
116
+ File.open(path) do |fd|
117
+ chunk = nil
118
+ size = 0
119
+ while chunk = fd.read(16384) do
120
+ size += io.write(chunk)
121
+ end
122
+ if size != stat.size
123
+ raise "Failed to add #{path} to the archive; expected to " +
124
+ "write #{stat.size} bytes, only wrote #{size}"
125
+ end
126
+ end
127
+ end # tar.tar.add_file_simple
128
+ end
129
+ end # def add_path
130
+
131
+ def to_s(format=nil)
132
+ return "#{name}-#{version}_#{iteration || 1}.txz"
133
+ return super(format)
134
+ end # def to_s
135
+ end # class FPM::Package::FreeBSD
136
+
137
+ # Backport Symlink Support to TarWriter
138
+ # https://github.com/rubygems/rubygems/blob/4a778c9c2489745e37bcc2d0a8f12c601a9c517f/lib/rubygems/package/tar_writer.rb#L239-L253
139
+ module TarWriterAddSymlink
140
+ refine Gem::Package::TarWriter do
141
+ def add_symlink(name, target, mode)
142
+ check_closed
143
+
144
+ name, prefix = split_name name
145
+
146
+ header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
147
+ :size => 0, :typeflag => "2",
148
+ :linkname => target,
149
+ :prefix => prefix,
150
+ :mtime => Time.now).to_s
151
+
152
+ @io.write header
153
+
154
+ self
155
+ end # def add_symlink
156
+ end # refine Gem::Package::TarWriter
157
+ end # module TarWriterAddSymlink
158
+
159
+ module Util
160
+ module Tar
161
+ unless Gem::Package::TarWriter.public_instance_methods.include? :add_symlink
162
+ using TarWriterAddSymlink
163
+ end
164
+ end # module Tar
165
+ end # module Util
@@ -81,7 +81,7 @@ class FPM::Package::OSXpkg < FPM::Package
81
81
  SCRIPT_MAP.each do |scriptname, filename|
82
82
  next unless script?(scriptname)
83
83
 
84
- with(scripts_path(filename)) do |pkgscript|
84
+ scripts_path(filename).tap do |pkgscript|
85
85
  logger.info("Writing pkg script", :source => filename, :target => pkgscript)
86
86
  File.write(pkgscript, script(scriptname))
87
87
  # scripts are required to be executable
@@ -103,7 +103,7 @@ class FPM::Package::OSXpkg < FPM::Package
103
103
 
104
104
  # Extract name and version from PackageInfo XML
105
105
  def extract_info(package)
106
- with(build_path("expand")) do |path|
106
+ build_path("expand").tap do |path|
107
107
  doc = REXML::Document.new File.open(File.join(path, "PackageInfo"))
108
108
  pkginfo_elem = doc.elements["pkg-info"]
109
109
  identifier = pkginfo_elem.attribute("identifier").value
@@ -0,0 +1,398 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "fpm/package"
3
+ require "fpm/util"
4
+ require "backports"
5
+ require "fileutils"
6
+ require "find"
7
+
8
+ class FPM::Package::Pacman < FPM::Package
9
+
10
+ option "--optional-depends", "PACKAGE",
11
+ "Add an optional dependency to the pacman package.", :multivalued => true
12
+
13
+ option "--use-file-permissions", :flag,
14
+ "Use existing file permissions when defining ownership and modes"
15
+
16
+ option "--user", "USER", "The owner of files in this package", :default => 'root'
17
+
18
+ option "--group", "GROUP", "The group owner of files in this package", :default => 'root'
19
+
20
+ # The list of supported compression types. Default is xz (LZMA2)
21
+ COMPRESSION_TYPES = [ "gz", "bzip2", "xz", "none" ]
22
+
23
+ option "--compression", "COMPRESSION", "The compression type to use, must " \
24
+ "be one of #{COMPRESSION_TYPES.join(", ")}.", :default => "xz" do |value|
25
+ if !COMPRESSION_TYPES.include?(value)
26
+ raise ArgumentError, "Pacman compression value of '#{value}' is invalid. " \
27
+ "Must be one of #{COMPRESSION_TYPES.join(", ")}"
28
+ end
29
+ value
30
+ end
31
+
32
+ def initialize(*args)
33
+ super(*args)
34
+ attributes[:pacman_optional_depends] = []
35
+ end # def initialize
36
+
37
+ def architecture
38
+ case @architecture
39
+ when nil
40
+ return %x{uname -m}.chomp # default to current arch
41
+ when "amd64" # debian and pacman disagree on architecture names
42
+ return "x86_64"
43
+ when "native"
44
+ return %x{uname -m}.chomp # 'native' is current arch
45
+ when "all", "any", "noarch"
46
+ return "any"
47
+ else
48
+ return @architecture
49
+ end
50
+ end # def architecture
51
+
52
+ def iteration
53
+ return @iteration || 1
54
+ end # def iteration
55
+
56
+ def config_files
57
+ return @config_files || []
58
+ end # def config_files
59
+
60
+ def dependencies
61
+ bogus_regex = /[^\sA-Za-z0-9><=-]/
62
+ # Actually modifies depencies if they are not right
63
+ bogus_dependencies = @dependencies.grep bogus_regex
64
+ if bogus_dependencies.any?
65
+ @dependencies.reject! { |a| a =~ bogus_regex }
66
+ logger.warn("Some of the dependencies looked like they weren't package " \
67
+ "names. Such dependency entries only serve to confuse arch. " \
68
+ "I am removing them.",
69
+ :removed_dependencies => bogus_dependencies,
70
+ :fixed_dependencies => @dependencies)
71
+ end
72
+ return @dependencies
73
+ end
74
+
75
+
76
+ # This method is invoked on a package when it has been converted to a new
77
+ # package format. The purpose of this method is to do any extra conversion
78
+ # steps, like translating dependency conditions, etc.
79
+ #def converted_from(origin)
80
+ # nothing to do by default. Subclasses may implement this.
81
+ # See the RPM package class for an example.
82
+ #end # def converted_from
83
+
84
+ # Add a new source to this package.
85
+ # The exact behavior depends on the kind of package being managed.
86
+ #
87
+ # For instance:
88
+ #
89
+ # * for FPM::Package::Dir, << expects a path to a directory or files.
90
+ # * for FPM::Package::RPM, << expects a path to an rpm.
91
+ #
92
+ # The idea is that you can keep pumping in new things to a package
93
+ # for later conversion or output.
94
+ #
95
+ # Implementations are expected to put files relevant to the 'input' in the
96
+ # staging_path
97
+ def input(pacman_pkg_path)
98
+ control = {}
99
+ # Unpack the control tarball
100
+ safesystem(tar_cmd, "-C", staging_path, "-xf", pacman_pkg_path)
101
+ pkginfo = staging_path(".PKGINFO")
102
+ mtree = staging_path(".MTREE")
103
+ install = staging_path(".INSTALL")
104
+
105
+ control_contents = File.read(pkginfo)
106
+ FileUtils.rm(pkginfo)
107
+ FileUtils.rm(mtree)
108
+
109
+
110
+ control_lines = control_contents.split("\n")
111
+ control_lines.each do |line|
112
+ key, val = line.split(/ += +/, 2)
113
+ if control.has_key? key
114
+ control[key].push(val)
115
+ else
116
+ control[key] = [val]
117
+ end
118
+ end
119
+
120
+ self.name = control["pkgname"][0]
121
+
122
+ # Parse 'epoch:version-iteration' in the version string
123
+ version_re = /^(?:([0-9]+):)?(.+?)(?:-(.*))?$/
124
+ m = version_re.match(control["pkgver"][0])
125
+ if !m
126
+ raise "Unsupported version string '#{control["pkgver"][0]}'"
127
+ end
128
+ self.epoch, self.version, self.iteration = m.captures
129
+
130
+ self.maintainer = control["packager"][0]
131
+
132
+ # Arch has no notion of vendor, so...
133
+ #self.vendor =
134
+
135
+ self.url = control["url"][0]
136
+ # Groups could include more than one.
137
+ # Speaking of just taking the first entry of the field:
138
+ # A crude thing to do, but I suppose it's better than nothing.
139
+ # -- Daniel Haskin, 3/24/2015
140
+ self.category = control["group"][0] || self.category
141
+
142
+ # Licenses could include more than one.
143
+ # Speaking of just taking the first entry of the field:
144
+ # A crude thing to do, but I suppose it's better than nothing.
145
+ # -- Daniel Haskin, 3/24/2015
146
+ self.license = control["license"][0] || self.license
147
+
148
+ self.architecture = control["arch"][0]
149
+
150
+ self.dependencies = control["depend"] || self.dependencies
151
+
152
+ self.provides = control["provides"] || self.provides
153
+
154
+ self.conflicts = control["conflict"] || self.conflicts
155
+
156
+ self.replaces = control["replaces"] || self.replaces
157
+
158
+ self.description = control["pkgdesc"][0]
159
+
160
+ if control.include? "backup"
161
+ self.config_files = control["backup"].map{|file| "/" + file}
162
+ else
163
+ self.config_files = []
164
+ end
165
+
166
+ self.dependencies = control["depend"] || self.dependencies
167
+
168
+ self.attributes[:pacman_optional_depends] = control["optdepend"] || []
169
+ # There are other available attributes, but I didn't include them because:
170
+ # - makedepend: deps needed to make the arch package. But it's already
171
+ # made. It just needs to be converted at this point
172
+ # - checkdepend: See above
173
+ # - makepkgopt: See above
174
+ # - size: can be dynamically generated
175
+ # - builddate: Should be changed to time of package conversion in the new
176
+ # package, so this value should be thrown away.
177
+
178
+ if File.exist?(install)
179
+ functions = parse_install_script(install)
180
+ if functions.include?("pre_install")
181
+ self.scripts[:before_install] = functions["pre_install"].join("\n")
182
+ end
183
+ if functions.include?("post_install")
184
+ self.scripts[:after_install] = functions["post_install"].join("\n")
185
+ end
186
+ if functions.include?("pre_upgrade")
187
+ self.scripts[:before_upgrade] = functions["pre_upgrade"].join("\n")
188
+ end
189
+ if functions.include?("post_upgrade")
190
+ self.scripts[:after_upgrade] = functions["post_upgrade"].join("\n")
191
+ end
192
+ if functions.include?("pre_remove")
193
+ self.scripts[:before_remove] = functions["pre_remove"].join("\n")
194
+ end
195
+ if functions.include?("post_remove")
196
+ self.scripts[:after_remove] = functions["post_remove"].join("\n")
197
+ end
198
+ FileUtils.rm(install)
199
+ end
200
+
201
+ # Note: didn't use `self.directories`.
202
+ # Pacman doesn't really record that information, to my knowledge.
203
+
204
+ end # def input
205
+
206
+ def compression_option
207
+ case self.attributes[:pacman_compression]
208
+ when nil, "xz"
209
+ return "--xz"
210
+ when "none"
211
+ return ""
212
+ when "gz"
213
+ return "-z"
214
+ when "bzip2"
215
+ return "-j"
216
+ else
217
+ return "--xz"
218
+ end
219
+ end
220
+
221
+ def compression_ending
222
+ case self.attributes[:pacman_compression]
223
+ when nil, "xz"
224
+ return ".xz"
225
+ when "none"
226
+ return ""
227
+ when "gz"
228
+ return ".gz"
229
+ when "bzip2"
230
+ return ".bz2"
231
+ else
232
+ return ".xz"
233
+ end
234
+ end
235
+
236
+ # Output this package to the given path.
237
+ def output(output_path)
238
+ output_check(output_path)
239
+
240
+ # Copy all files from staging to BUILD dir
241
+ Find.find(staging_path) do |path|
242
+ src = path.gsub(/^#{staging_path}/, '')
243
+ dst = build_path(src)
244
+ copy_entry(path, dst, preserve=true, remove_destination=true)
245
+ copy_metadata(path, dst)
246
+ end
247
+
248
+ # This value is used later in the template for PKGINFO
249
+ size = safesystemout("du", "-sk", build_path).split(/\s+/)[0].to_i * 1024
250
+ builddate = Time.new.to_i
251
+
252
+ pkginfo = template("pacman.erb").result(binding)
253
+ pkginfo_file = build_path(".PKGINFO")
254
+ File.write(pkginfo_file, pkginfo)
255
+
256
+ if script?(:before_install) or script?(:after_install) or \
257
+ script?(:before_upgrade) or script?(:after_upgrade) or \
258
+ script?(:before_remove) or script?(:after_remove)
259
+ install_script = template("pacman/INSTALL.erb").result(binding)
260
+ install_script_file = build_path(".INSTALL")
261
+ File.write(install_script_file, install_script)
262
+ end
263
+
264
+ generate_mtree
265
+
266
+ File.expand_path(output_path).tap do |path|
267
+ ::Dir.chdir(build_path) do
268
+ safesystem(*([tar_cmd,
269
+ compression_option,
270
+ "-cf",
271
+ path] + data_tar_flags + \
272
+ ::Dir.entries(".").reject{|entry| entry =~ /^\.{1,2}$/ }))
273
+ end
274
+ end
275
+ end # def output
276
+
277
+ def data_tar_flags
278
+ data_tar_flags = []
279
+ if attributes[:pacman_use_file_permissions?].nil?
280
+ if !attributes[:pacman_user].nil?
281
+ if attributes[:pacman_user] == 'root'
282
+ data_tar_flags += [ "--numeric-owner", "--owner", "0" ]
283
+ else
284
+ data_tar_flags += [ "--owner", attributes[:deb_user] ]
285
+ end
286
+ end
287
+
288
+ if !attributes[:pacman_group].nil?
289
+ if attributes[:pacman_group] == 'root'
290
+ data_tar_flags += [ "--numeric-owner", "--group", "0" ]
291
+ else
292
+ data_tar_flags += [ "--group", attributes[:deb_group] ]
293
+ end
294
+ end
295
+ end
296
+ return data_tar_flags
297
+ end # def data_tar_flags
298
+
299
+ def default_output
300
+ v = version
301
+ v = "#{epoch}:#{v}" if epoch
302
+ if iteration
303
+ "#{name}_#{v}-#{iteration}_#{architecture}.#{type}"
304
+ else
305
+ "#{name}_#{v}_#{architecture}.#{type}"
306
+ end
307
+ end # def default_output
308
+
309
+ def to_s(format=nil)
310
+ # Default format if nil
311
+ # git_1.7.9.3-1_amd64.deb
312
+ return super("NAME-FULLVERSION-ARCH.pkg.tar#{compression_ending}") if format.nil?
313
+ return super(format)
314
+ end # def to_s
315
+
316
+ private
317
+
318
+ def generate_mtree
319
+ ::Dir.chdir(build_path) do
320
+ cmd = "LANG=C bsdtar "
321
+ cmd += "-czf .MTREE "
322
+ cmd += "--format=mtree "
323
+ cmd += "--options='!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link' "
324
+ cmd += ::Dir.entries(".").reject{|entry| entry =~ /^\.{1,2}$/ }.join(" ")
325
+ safesystem(cmd)
326
+ end
327
+ end # def generate_mtree
328
+
329
+ # KNOWN ISSUE:
330
+ # If an un-matched bracket is used in valid bash, as in
331
+ # `echo "{"`, this function will choke.
332
+ # However, to cover this case basically
333
+ # requires writing almost half a bash parser,
334
+ # and it is a very small corner case.
335
+ # Otherwise, this approach is very robust.
336
+ def gobble_function(cons,prod)
337
+ level = 1
338
+ while level > 0
339
+ line = cons.next
340
+ # Not the best, but pretty good
341
+ # short of writing an *actual* sh
342
+ # parser
343
+ level += line.count "{"
344
+ level -= line.count "}"
345
+ if level > 0
346
+ prod.push(line.rstrip())
347
+ else
348
+ fine = line.sub(/\s*[}]\s*$/, "")
349
+ if !(fine =~ /^\s*$/)
350
+ prod.push(fine.rstrip())
351
+ end
352
+ end
353
+ end
354
+ end # def gobble_function
355
+
356
+ FIND_SCRIPT_FUNCTION_LINE =
357
+ /^\s*(\w+)\s*\(\s*\)\s*\{\s*([^}]+?)?\s*(\})?\s*$/
358
+
359
+ def parse_install_script(path)
360
+ global_lines = []
361
+ look_for = Set.new(["pre_install", "post_install",
362
+ "pre_upgrade", "post_upgrade",
363
+ "pre_remove", "post_remove"])
364
+ functions = {}
365
+ look_for.each do |fname|
366
+ functions[fname] = []
367
+ end
368
+
369
+ open(path, "r") do |iscript|
370
+ lines = iscript.each
371
+ begin
372
+ while true
373
+ line = lines.next
374
+ # This regex picks up beginning names of posix shell
375
+ # functions
376
+ # Examples:
377
+ # fname() {
378
+ # fname() { echo hi }
379
+ m = FIND_SCRIPT_FUNCTION_LINE.match(line)
380
+ if not m.nil? and look_for.include? m[1]
381
+ if not m[2].nil?
382
+ functions[m[1]].push(m[2].rstrip())
383
+ end
384
+ gobble_function(lines, functions[m[1]])
385
+ else
386
+ global_lines.push(line.rstrip())
387
+ end
388
+ end
389
+ rescue StopIteration
390
+ end
391
+ end
392
+ look_for.each do |name|
393
+ # Add global lines to each function to preserve global variables, etc.
394
+ functions[name] = global_lines + functions[name]
395
+ end
396
+ return functions
397
+ end # def parse_install_script
398
+ end # class FPM::Package::Pacman