Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

467 lines (407 sloc) 15.969 kb
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
option "--priority", "PRIORITY",
"The debian package 'priority' value.", :default => "extra"
option "--user", "USER", "The owner of files in this package"
option "--group", "GROUP", "The group owner of files in this package"
def initialize(*args)
super(*args)
attributes[:deb_priority] = "extra"
end # def initialize
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
if $?.exitstatus != 0 or @architecture.empty?
# if dpkg fails or emits nothing, revert back to uname -m
@architecture = %x{uname -m}.chomp
end
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. I have downcased the " \
"package name for you just to be safe.",
:oldname => @name, :fixedname => @name.downcase)
@name = @name.downcase
end
if @name.include?("_")
@logger.info("Debian package names cannot include underscores; " \
"automatically 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
@logger.info("deb field", field => value.split(": ", 2).last)
return 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
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)
output_check(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
tar_flags = []
if !attributes[:deb_user].nil?
tar_flags += [ "--owner", attributes[:deb_user] ]
end
if !attributes[:deb_group].nil?
tar_flags += [ "--group", attributes[:deb_group] ]
end
safesystem(tar_cmd, "-C", staging_path, compression, *tar_flags, "-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
Jump to Line
Something went wrong with that request. Please try again.