class Package::Deb

Support for debian packages (.deb files)

This class supports both input and output of packages.

Constants

COMPRESSION_TYPES

The list of supported compression types. Default is gz (gzip)

SCRIPT_MAP

Map of what scripts are named.

Public Class Methods

new(*args) click to toggle source
Calls superclass method Package::new
# File lib/fpm/package/deb.rb, line 147
def initialize(*args)
  super(*args)
  attributes[:deb_priority] = "extra"
end

Public Instance Methods

architecture() click to toggle source

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.

# File lib/fpm/package/deb.rb, line 157
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

  case @architecture
  when "x86_64"
    # Debian calls x86_64 "amd64"
    @architecture = "amd64"
  when "noarch"
    # Debian calls noarch "all"
    @architecture = "all"
  end
  return @architecture
end
converted_from(origin) click to toggle source
# File lib/fpm/package/deb.rb, line 447
def converted_from(origin)
  self.dependencies = self.dependencies.collect do |dep|
    fix_dependency(dep)
  end.flatten
  self.provides = self.provides.collect do |provides|
    fix_provides(provides)
  end.flatten

  if origin == FPM::Package::Deb
    changelog_path = staging_path("usr/share/doc/#{name}/changelog.Debian.gz")
    if File.exists?(changelog_path)
      logger.debug("Found a deb changelog file, using it.", :path => changelog_path)
      attributes[:deb_changelog] = build_path("deb_changelog")
      File.open(attributes[:deb_changelog], "w") do |deb_changelog|
        Zlib::GzipReader.open(changelog_path) do |gz|
          IO::copy_stream(gz, deb_changelog)
        end
      end
      File.unlink(changelog_path)
    end
  end
end
data_tar_flags() click to toggle source
# File lib/fpm/package/deb.rb, line 748
def data_tar_flags
  data_tar_flags = []
  if attributes[:deb_use_file_permissions?].nil?
    if !attributes[:deb_user].nil?
      if attributes[:deb_user] == 'root'
        data_tar_flags += [ "--numeric-owner", "--owner", "0" ]
      else
        data_tar_flags += [ "--owner", attributes[:deb_user] ]
      end
    end

    if !attributes[:deb_group].nil?
      if attributes[:deb_group] == 'root'
        data_tar_flags += [ "--numeric-owner", "--group", "0" ]
      else
        data_tar_flags += [ "--group", attributes[:deb_group] ]
      end
    end
  end
  return data_tar_flags
end
input(input_path) click to toggle source
# File lib/fpm/package/deb.rb, line 217
def input(input_path)
  extract_info(input_path)
  extract_files(input_path)
end
name() click to toggle source

Get the name of this package. See also Package#name

This accessor actually modifies the name if it has some invalid or unwise characters.

# File lib/fpm/package/deb.rb, line 187
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

  if @name.include?(" ")
    logger.info("Debian package names cannot include spaces; "                     "automatically converting to dashes", :name => @name)
    @name = @name.gsub(/[ ]/, "-")
  end

  return @name
end
output(output_path) click to toggle source
# File lib/fpm/package/deb.rb, line 329
def output(output_path)
  self.provides = self.provides.collect { |p| fix_provides(p) }
  output_check(output_path)
  # Abort if the target path already exists.

  # create 'debian-binary' file, required to make a valid debian package
  File.write(build_path("debian-binary"), "2.0\n")

  # If we are given --deb-shlibs but no --after-install script, we
  # should implicitly create a before/after scripts that run ldconfig
  if attributes[:deb_shlibs]
    if !script?(:after_install)
      logger.info("You gave --deb-shlibs but no --after-install, so "                       "I am adding an after-install script that runs "                       "ldconfig to update the system library cache")
      scripts[:after_install] = template("deb/ldconfig.sh.erb").result(binding)
    end
    if !script?(:after_remove)
      logger.info("You gave --deb-shlibs but no --after-remove, so "                       "I am adding an after-remove script that runs "                       "ldconfig to update the system library cache")
      scripts[:after_remove] = template("deb/ldconfig.sh.erb").result(binding)
    end
  end

  if script?(:before_upgrade) or script?(:after_upgrade)
    if script?(:before_install) or script?(:before_upgrade)
      scripts[:before_install] = template("deb/preinst_upgrade.sh.erb").result(binding)
    end
    if script?(:before_remove)
      scripts[:before_remove] = template("deb/prerm_upgrade.sh.erb").result(binding)
    end
    if script?(:after_install) or script?(:after_upgrade)
      scripts[:after_install] = template("deb/postinst_upgrade.sh.erb").result(binding)
    end
    if script?(:after_remove)
      scripts[:after_remove] = template("deb/postrm_upgrade.sh.erb").result(binding)
    end
  end

  write_control_tarball

  # Tar up the staging_path into data.tar.{compression type}
  case self.attributes[:deb_compression]
    when "gz", 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

  # Write the changelog file
  dest_changelog = File.join(staging_path, "usr/share/doc/#{name}/changelog.Debian.gz")
  FileUtils.mkdir_p(File.dirname(dest_changelog))
  File.new(dest_changelog, "wb", 0644).tap do |changelog|
    Zlib::GzipWriter.new(changelog, Zlib::BEST_COMPRESSION).tap do |changelog_gz|
      if attributes[:deb_changelog]
        logger.info("Writing user-specified changelog", :source => attributes[:deb_changelog])
        File.new(attributes[:deb_changelog]).tap do |fd|
          chunk = nil
          # Ruby 1.8.7 doesn't have IO#copy_stream
          changelog_gz.write(chunk) while chunk = fd.read(16384)
        end.close
      else
        logger.info("Creating boilerplate changelog file")
        changelog_gz.write(template("deb/changelog.erb").result(binding))
      end
    end.close
  end # No need to close, GzipWriter#close will close it.

  attributes.fetch(:deb_init_list, []).each do |init|
    name = File.basename(init, ".init")
    dest_init = File.join(staging_path, "etc/init.d/#{name}")
    FileUtils.mkdir_p(File.dirname(dest_init))
    FileUtils.cp init, dest_init
    File.chmod(0755, dest_init)
  end

  attributes.fetch(:deb_default_list, []).each do |default|
    name = File.basename(default, ".default")
    dest_default = File.join(staging_path, "etc/default/#{name}")
    FileUtils.mkdir_p(File.dirname(dest_default))
    FileUtils.cp default, dest_default
    File.chmod(0644, dest_default)
  end

  attributes.fetch(:deb_upstart_list, []).each do |upstart|
    name = File.basename(upstart, ".upstart")
    dest_upstart = staging_path("etc/init/#{name}.conf")
    FileUtils.mkdir_p(File.dirname(dest_upstart))
    FileUtils.cp(upstart, dest_upstart)
    File.chmod(0644, dest_upstart)

    # Install an init.d shim that calls upstart
    dest_init = staging_path("/etc/init.d/#{name}")
    FileUtils.mkdir_p(File.dirname(dest_init))
    FileUtils.ln_s("/lib/init/upstart-job", dest_init)
  end

  args = [ tar_cmd, "-C", staging_path, compression ] + data_tar_flags + [ "-cf", datatar, "." ]
  safesystem(*args)

  # 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
end
prefix() click to toggle source
# File lib/fpm/package/deb.rb, line 213
def prefix
  return (attributes[:prefix] or "/")
end
to_s(format=nil) click to toggle source
Calls superclass method Package#to_s
# File lib/fpm/package/deb.rb, line 741
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

Private Instance Methods

control_path(path=nil) click to toggle source
# File lib/fpm/package/deb.rb, line 554
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
debianize_op(op) click to toggle source
# File lib/fpm/package/deb.rb, line 470
def debianize_op(op)
  # Operators in debian packaging are <<, <=, =, >= and >>
  # So any operator like < or > must be replaced
  {:< => "<<", :> => ">>"}[op.to_sym] or op
end
extract_files(package) click to toggle source
# File lib/fpm/package/deb.rb, line 305
def extract_files(package)
  # Find out the compression type
  compression = %xar 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
extract_info(package) click to toggle source
# File lib/fpm/package/deb.rb, line 222
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
    with(parse.call("Provides")) do |provides_str|
      next if provides_str.nil?
      self.provides = provides_str.split(/\s*,\s*/)
    end

    # 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: .*/].split(": ", 2).last
    self.description = description.gsub(/^ /, "").gsub(/^\.$/, "")

    #self.config_files = config_files

    self.dependencies += parse_depends(parse.call("Depends")) if !attributes[:no_auto_depends?]
  end
end
fix_dependency(dep) click to toggle source
# File lib/fpm/package/deb.rb, line 476
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 = dep.gsub(name_re) { |n| n.downcase }
  end

  if dep.include?("_")
    logger.warn("Replacing dependency underscores with dashes in '#{dep}' because "                     "debs don't like underscores")
    dep = 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+\(!= (.+)\)/))
    # Move '!=' dependency specifications into 'Breaks'
    self.attributes[:deb_breaks] ||= []
    self.attributes[:deb_breaks] << 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})"]
  elsif (m = dep.match(/(\S+)\s+\(> (.+)\)/))
    # Convert 'foo (> x) to 'foo (>> x)'
    name, version = m[1..2]
    return ["#{name} (>> #{version})"]
  else
    # otherwise the dep is probably fine
    return dep.rstrip
  end
end
fix_provides(provides) click to toggle source
# File lib/fpm/package/deb.rb, line 537
def fix_provides(provides)
  name_re = /^[^ \(]+/
  name = provides[name_re]
  if name =~ /[A-Z]/
    logger.warn("Downcasing provides '#{name}' because deb packages "                     " don't work so good with uppercase names")
    provides = provides.gsub(name_re) { |n| n.downcase }
  end

  if provides.include?("_")
    logger.warn("Replacing 'provides' underscores with dashes in '#{provides}' because "                     "debs don't like underscores")
    provides = provides.gsub("_", "-")
  end
  return provides.rstrip
end
parse_depends(data) click to toggle source

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")
# File lib/fpm/package/deb.rb, line 280
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
write_conffiles() click to toggle source
# File lib/fpm/package/deb.rb, line 643
def write_conffiles
  return unless config_files.any?

  # scan all conf file paths for files and add them
  allconfigs = []
  config_files.each do |path|
    # Strip leading /
    path = path[1..-1] if path[0,1] == "/"
    cfg_path = File.expand_path(path, staging_path)
    begin
      Find.find(cfg_path) do |p|
        allconfigs << p.gsub("#{staging_path}/", '') if File.file? p
      end
    rescue Errno::ENOENT => e
      raise FPM::InvalidPackageConfiguration,
        "Error trying to use '#{cfg_path}' as a config file in the package. Does it exist?"
    end
  end
  allconfigs.sort!.uniq!

  with(control_path("conffiles")) do |conffiles|
    File.open(conffiles, "w") do |out|
      # 'config_files' comes from FPM::Package and is usually set with
      # FPM::Command's --config-files flag
      allconfigs.each do |cf|
        # We need to put the leading / back. Stops lintian relative-conffile error.
        out.puts("/" + cf)
      end
    end
    File.chmod(0644, conffiles)
  end
end
write_control() click to toggle source
# File lib/fpm/package/deb.rb, line 590
def write_control
  # warn user if epoch is set
  logger.warn("epoch in Version is set", :epoch => self.epoch) if self.epoch

  # 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)
    File.chmod(0644, control)
    edit_file(control) if attributes[:edit?]
  end
end
write_control_tarball() click to toggle source
# File lib/fpm/package/deb.rb, line 565
def write_control_tarball
  # Use custom Debian control file when given ...
  write_control # write the control file
  write_shlibs # write optional shlibs file
  write_scripts # write the maintainer scripts
  write_conffiles # write the conffiles
  write_debconf # write the debconf files
  write_meta_files # write additional meta files
  write_triggers # write trigger config to 'triggers' file
  write_md5sums # write the md5sums file

  # Make the control.tar.gz
  with(build_path("control.tar.gz")) do |controltar|
    logger.info("Creating", :path => controltar, :from => control_path)

    args = [ tar_cmd, "-C", control_path, "-zcf", controltar, 
      "--owner=0", "--group=0", "--numeric-owner", "." ]
    safesystem(*args)
  end

  logger.debug("Removing no longer needed control dir", :path => control_path)
ensure
  FileUtils.rm_r(control_path)
end
write_debconf() click to toggle source
# File lib/fpm/package/deb.rb, line 684
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
write_md5sums() click to toggle source
# File lib/fpm/package/deb.rb, line 720
def write_md5sums
  md5_sums = {}

  Find.find(staging_path) do |path|
    if File.file?(path) && !File.symlink?(path)
      md5 = Digest::MD5.file(path).hexdigest
      md5_path = path.gsub("#{staging_path}/", "")
      md5_sums[md5_path] = md5
    end
  end

  if not md5_sums.empty?
    File.open(control_path("md5sums"), "w") do |out|
      md5_sums.each do |path, md5|
        out.puts "#{md5}  #{path}"
      end
    end
    File.chmod(0644, control_path("md5sums"))
  end
end
write_meta_files() click to toggle source
# File lib/fpm/package/deb.rb, line 696
def write_meta_files
  files = attributes[:deb_meta_files]
  return unless files
  files.each do |fn|
    dest = control_path(File.basename(fn))
    FileUtils.cp(fn, dest)
    File.chmod(0644, dest)
  end
end
write_scripts() click to toggle source

Write out the maintainer scripts

SCRIPT_MAP is a map from the package ':after_install' to debian 'post_install' names

# File lib/fpm/package/deb.rb, line 630
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
write_shlibs() click to toggle source
# File lib/fpm/package/deb.rb, line 676
def write_shlibs
  return unless attributes[:deb_shlibs]
  logger.info("Adding shlibs", :content => attributes[:deb_shlibs])
  File.open(control_path("shlibs"), "w") do |out|
    out.write(attributes[:deb_shlibs])
  end
end
write_triggers() click to toggle source
# File lib/fpm/package/deb.rb, line 706
def write_triggers
  lines = [['interest', :deb_interest],
           ['activate', :deb_activate]].map { |label, attr|
    (attributes[attr] || []).map { |e| "#{label} #{e}\n" }
  }.flatten.join('')

  if lines.size > 0
    File.open(control_path("triggers"), 'a') do |f|
      f.write "\n" if f.size > 0
      f.write lines
    end
  end
end