/
deb.rb
442 lines (387 loc) · 14.7 KB
/
deb.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
require "erb"
require "fpm/namespace"
require "fpm/package"
require "fpm/errors"
require "fpm/util"
require "backports"
require "fileutils"
# Support for debian packages (.deb files)
#
# This class supports both input and output of packages.
class FPM::Package::Deb < FPM::Package
# Map of what scripts are named.
SCRIPT_MAP = {
:before_install => "preinst",
:after_install => "postinst",
:before_remove => "prerm",
:after_remove => "postrm",
} unless defined?(SCRIPT_MAP)
# The list of supported compression types. Default is gz (gzip)
COMPRESSION_TYPES = [ "gz", "bzip2", "xz" ]
option "--ignore-iteration-in-dependencies", :flag,
"For '=' (equal) dependencies, allow iterations on the specified " \
"version. Default is to be specific. This option allows the same " \
"version of a package but any iteration is permitted"
option "--pre-depends", "DEPENDENCY",
"Add DEPENDENCY as a Pre-Depends" do |dep|
@pre_depends ||= []
@pre_depends << dep
end
option "--compression", "COMPRESSION", "The compression type to use, must " \
"be one of #{COMPRESSION_TYPES.join(", ")}.", :default => "gzip" do |value|
if !COMPRESSION_TYPES.include?(value)
raise ArgumentError, "deb compression value of '#{value}' is invalid. " \
"Must be one of #{COMPRESSION_TYPES.join(", ")}"
end
value
end
# Take care about the case when we want custom control file but still use fpm ...
option "--custom-control", "FILEPATH",
"Custom version of the Debian control file." do |control|
File.expand_path(control)
end
# Add custom debconf config file
option "--config", "SCRIPTPATH",
"Add SCRIPTPATH as debconf config file." do |config|
File.expand_path(config)
end
# Add custom debconf templates file
option "--templates", "FILEPATH",
"Add FILEPATH as debconf templates file." do |templates|
File.expand_path(templates)
end
option "--installed-size", "KILOBYTES",
"The installed size, in kilobytes. If omitted, this will be calculated " \
"automatically" do |value|
value.to_i
end
private
# Return the architecture. This will default to native if not yet set.
# It will also try to use dpkg and 'uname -m' to figure out what the
# native 'architecture' value should be.
def architecture
if @architecture.nil? or @architecture == "native"
# Default architecture should be 'native' which we'll need to ask the
# system about.
if program_in_path?("dpkg")
@architecture = %x{dpkg --print-architecture 2> /dev/null}.chomp
@architecture = %{uname -m}.chomp if $?.exitstatus != 0
else
@architecture = %x{uname -m}.chomp
end
end
if @architecture == "x86_64"
# Debian calls x86_64 "amd64"
@architecture = "amd64"
end
return @architecture
end # def architecture
# Get the name of this package. See also FPM::Package#name
#
# This accessor actually modifies the name if it has some invalid or unwise
# characters.
def name
if @name =~ /[A-Z]/
@logger.warn("Debian tools (dpkg/apt) don't do well with packages " \
"that use capital letters in the name. In some cases it will " \
"automatically downcase them, in others it will not. It is confusing." \
"Best to not use any capital letters at all.",
:oldname => @name, :fixedname => @name.downcase)
@name = @name.downcase
end
if @name.include?("_")
@logger.info("Package name includes underscores, converting to dashes",
:name => @name)
@name = @name.gsub(/[_]/, "-")
end
return @name
end # def name
def input(input_path)
extract_info(input_path)
extract_files(input_path)
end # def input
def extract_info(package)
with(build_path("control")) do |path|
FileUtils.mkdir(path) if !File.directory?(path)
# Unpack the control tarball
safesystem("ar p #{package} control.tar.gz | tar -zxf - -C #{path}")
control = File.read(File.join(path, "control"))
parse = lambda do |field|
value = control[/^#{field.capitalize}: .*/]
if value.nil?
return nil
else
value.split(": ",2).last
end
end
# Parse 'epoch:version-iteration' in the version string
version_re = /^(?:([0-9]+):)?(.+?)(?:-(.*))?$/
m = version_re.match(parse.call("Version"))
if !m
raise "Unsupported version string '#{parse.call("Version")}'"
end
self.epoch, self.version, self.iteration = m.captures
self.architecture = parse.call("Architecture")
self.category = parse.call("Section")
self.license = parse.call("License") || self.license
self.maintainer = parse.call("Maintainer")
self.name = parse.call("Package")
self.url = parse.call("Homepage")
self.vendor = parse.call("Vendor") || self.vendor
# The description field is a special flower, parse it that way.
# The description is the first line as a normal Description field, but also continues
# on future lines indented by one space, until the end of the file. Blank
# lines are marked as ' .'
description = control[/^Description: .*/m].split(": ", 2).last
self.description = description.gsub(/^ /, "").gsub(/^\.$/, "")
#self.config_files = config_files
self.dependencies += parse_depends(parse.call("Depends"))
end
end # def extract_info
# Parse a 'depends' line from a debian control file.
#
# The expected input 'data' should be everything after the 'Depends: ' string
#
# Example:
#
# parse_depends("foo (>= 3), bar (= 5), baz")
def parse_depends(data)
return [] if data.nil? or data.empty?
# parse dependencies. Debian dependencies come in one of two forms:
# * name
# * name (op version)
# They are all on one line, separated by ", "
dep_re = /^([^ ]+)(?: \(([>=<]+) ([^)]+)\))?$/
return data.split(/, */).collect do |dep|
m = dep_re.match(dep)
if m
name, op, version = m.captures
# deb uses ">>" and "<<" for greater and less than respectively.
# fpm wants just ">" and "<"
op = "<" if op == "<<"
op = ">" if op == ">>"
# this is the proper form of dependency
"#{name} #{op} #{version}"
else
# Assume normal form dependency, "name op version".
dep
end
end
end # def parse_depends
def extract_files(package)
# Find out the compression type
p `ar t #{package}`
compression = `ar t #{package}`.split("\n").grep(/data.tar/).first.split(".").last
case compression
when "gz"
datatar = "data.tar.gz"
compression = "-z"
when "bzip2"
datatar = "data.tar.bz2"
compression = "-j"
when "xz"
datatar = "data.tar.xz"
compression = "-J"
else
raise FPM::InvalidPackageConfiguration,
"Unknown compression type '#{self.attributes[:deb_compression]}' "
"in deb source package #{package}"
end
# unpack the data.tar.{gz,bz2,xz} from the deb package into staging_path
safesystem("ar p #{package} #{datatar} " \
"| tar #{compression} -xf - -C #{staging_path}")
end # def extract_files
def output(output_path)
# Abort if the target path already exists.
raise FileAlreadyExists.new(output_path) if File.exists?(output_path)
# create 'debian-binary' file, required to make a valid debian package
File.write(build_path("debian-binary"), "2.0\n")
write_control_tarball
# Tar up the staging_path into data.tar.{compression type}
case self.attributes[:deb_compression]
when "gzip", nil
datatar = build_path("data.tar.gz")
compression = "-z"
when "bzip2"
datatar = build_path("data.tar.bz2")
compression = "-j"
when "xz"
datatar = build_path("data.tar.xz")
compression = "-J"
else
raise FPM::InvalidPackageConfiguration,
"Unknown compression type '#{self.attributes[:deb_compression]}'"
end
safesystem(tar_cmd, "-C", staging_path, compression, "-cf", datatar, ".")
# pack up the .deb, which is just an 'ar' archive with 3 files
# the 'debian-binary' file has to be first
with(File.expand_path(output_path)) do |output_path|
::Dir.chdir(build_path) do
safesystem("ar", "-qc", output_path, "debian-binary", "control.tar.gz", datatar)
end
end
@logger.log("Created deb package", :path => output_path)
end # def output
def default_output
if iteration
"#{name}_#{version}-#{iteration}_#{architecture}.#{type}"
else
"#{name}_#{version}_#{architecture}.#{type}"
end
end # def default_output
def converted_from(origin)
self.dependencies = self.dependencies.collect do |dep|
fix_dependency(dep)
end.flatten
end # def converted_from
def debianize_op(op)
# Operators in debian packaging are <<, <=, =, >= and >>
# So any operator like < or > must be replaced
{:< => "<<", :> => ">>"}[op.to_sym] or op
end
def fix_dependency(dep)
# Deb dependencies are: NAME (OP VERSION), like "zsh (> 3.0)"
# Convert anything that looks like 'NAME OP VERSION' to this format.
if dep =~ /[\(,\|]/
# Don't "fix" ones that could appear well formed already.
else
# Convert ones that appear to be 'name op version'
name, op, version = dep.split(/ +/)
if !version.nil?
# Convert strings 'foo >= bar' to 'foo (>= bar)'
dep = "#{name} (#{debianize_op(op)} #{version})"
end
end
name_re = /^[^ \(]+/
name = dep[name_re]
if name =~ /[A-Z]/
@logger.warn("Downcasing dependency '#{name}' because deb packages " \
" don't work so good with uppercase names")
dep.gsub!(name_re) { |n| n.downcase }
end
if dep.include?("_")
@logger.warn("Replacing underscores with dashes in '#{dep}' because " \
"debs don't like underscores")
dep.gsub!("_", "-")
end
# Convert gem ~> X.Y.Z to '>= X.Y.Z' and << X.Y+1.0
if dep =~ /\(~>/
name, version = dep.gsub(/[()~>]/, "").split(/ +/)[0..1]
nextversion = version.split(".").collect { |v| v.to_i }
l = nextversion.length
nextversion[l-2] += 1
nextversion[l-1] = 0
nextversion = nextversion.join(".")
return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"]
elsif (m = dep.match(/(\S+)\s+\(!= (.+)\)/))
# Append this to conflicts
self.conflicts += [dep.gsub(/!=/,"=")]
return []
elsif (m = dep.match(/(\S+)\s+\(= (.+)\)/)) and
self.attributes[:deb_ignore_iteration_in_dependencies?]
# Convert 'foo (= x)' to 'foo (>= x)' and 'foo (<< x+1)'
# but only when flag --ignore-iteration-in-dependencies is passed.
name, version = m[1..2]
nextversion = version.split('.').collect { |v| v.to_i }
nextversion[-1] += 1
nextversion = nextversion.join(".")
return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"]
else
# otherwise the dep is probably fine
return dep.rstrip
end
end # def fix_dependency
def control_path(path=nil)
@control_path ||= build_path("control")
FileUtils.mkdir(@control_path) if !File.directory?(@control_path)
if path.nil?
return @control_path
else
return File.join(@control_path, path)
end
end # def control_path
def write_control_tarball
# Use custom Debian control file when given ...
write_control # write the control file
write_scripts # write the maintainer scripts
write_conffiles # write the conffiles
write_debconf # write the debconf files
# Make the control.tar.gz
with(build_path("control.tar.gz")) do |controltar|
@logger.info("Creating", :path => controltar, :from => control_path)
safesystem(tar_cmd, "--numeric-owner", "--owner=0", "--group=0", "-zcf",
controltar, "-C", control_path, ".")
end
@logger.debug("Removing no longer needed control dir", :path => control_path)
ensure
FileUtils.rm_r(control_path)
end # def write_control_tarball
def write_control
# calculate installed-size if necessary:
if attributes[:deb_installed_size].nil?
@logger.info("No deb_installed_size set, calculating now.")
total = 0
Find.find(staging_path) do |path|
stat = File.lstat(path)
next if stat.directory?
total += stat.size
end
# Per http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Installed-Size
# "The disk space is given as the integer value of the estimated
# installed size in bytes, divided by 1024 and rounded up."
attributes[:deb_installed_size] = total / 1024
end
# Write the control file
with(control_path("control")) do |control|
if attributes[:deb_custom_control]
@logger.debug("Using '#{attributes[:deb_custom_control]}' template for the control file")
control_data = File.read(attributes[:deb_custom_control])
else
@logger.debug("Using 'deb.erb' template for the control file")
control_data = template("deb.erb").result(binding)
end
@logger.debug("Writing control file", :path => control)
File.write(control, control_data)
edit_file(control) if attributes[:edit?]
end
end # def write_control
# Write out the maintainer scripts
#
# SCRIPT_MAP is a map from the package ':after_install' to debian
# 'post_install' names
def write_scripts
SCRIPT_MAP.each do |scriptname, filename|
next unless script?(scriptname)
with(control_path(filename)) do |controlscript|
@logger.debug("Writing control script", :source => filename, :target => controlscript)
File.write(controlscript, script(scriptname))
# deb maintainer scripts are required to be executable
File.chmod(0755, controlscript)
end
end
end # def write_scripts
def write_conffiles
File.open(control_path("conffiles"), "w") do |out|
# 'config_files' comes from FPM::Package and is usually set with
# FPM::Command's --config-files flag
config_files.each { |cf| out.puts(cf) }
end
end # def write_conffiles
def write_debconf
if attributes[:deb_config]
FileUtils.cp(attributes[:deb_config], control_path("config"))
File.chmod(0755, control_path("config"))
end
if attributes[:deb_templates]
FileUtils.cp(attributes[:deb_templates], control_path("templates"))
File.chmod(0755, control_path("templates"))
end
end # def write_debconf
def to_s(format=nil)
# Default format if nil
# git_1.7.9.3-1_amd64.deb
return super("NAME_FULLVERSION_ARCH.TYPE") if format.nil?
return super(format)
end # def to_s
public(:input, :output, :architecture, :name, :converted_from, :to_s)
end # class FPM::Target::Deb