fpm 1.4.0 → 1.5.0

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.
@@ -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