class FPM::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)

RELATIONSHIP_FIELD_PATTERN
SCRIPT_MAP

Map of what scripts are named.

VERSION_FIELD_PATTERN

Version string docs here: www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version The format is: [epoch:]upstream_version. epoch - This is a single (generally small) unsigned integer upstream_version - must contain only alphanumerics 6 and the characters . + - ~ debian_revision - only alphanumerics and the characters + . ~

Public Class Methods

new(*args) click to toggle source
Calls superclass method FPM::Package::new
# File lib/fpm/package/deb.rb, line 248
def initialize(*args)
  super(*args)
  attributes[:deb_priority] = "optional"
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 258
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 "aarch64"
    # Debian calls aarch64 "arm64"
    @architecture = "arm64"
  when "noarch"
    # Debian calls noarch "all"
    @architecture = "all"
  when "ppc64le"
    # Debian calls ppc64le "ppc64el"
    @architecture = "ppc64el"
  end
  return @architecture
end
converted_from(origin) click to toggle source
# File lib/fpm/package/deb.rb, line 753
def converted_from(origin)
  self.dependencies = self.dependencies.collect do |dep|
    fix_dependency(dep)
  end.flatten

  # If an invalid depends field was found i.e. /bin.sh then fix_depends will blank it
  # Make sure we remove this blank here
  self.dependencies = self.dependencies.reject { |p| p.empty? }

  self.provides = self.provides.collect do |provides|
    fix_provides(provides)
  end.flatten
  # If an invalid provides field was found i.e. mypackage(arch) then fix_provides will blank it
  # Make sure we remove this blank here
  self.provides = self.provides.reject { |p| p.empty? }

  if origin == FPM::Package::CPAN
    # The fpm cpan code presents dependencies and provides fields as perl(ModuleName)
    # so we'll need to convert them to something debian supports.

    # Replace perl(ModuleName) > 1.0 with Debian-style perl-ModuleName (> 1.0)
    perldepfix = lambda do |dep|
      m = dep.match(/perl\((?<name>[A-Za-z0-9_:]+)\)\s*(?<op>.*$)/)
      if m.nil?
        # 'dep' syntax didn't look like 'perl(Name) > 1.0'
        dep
      else
        # Also replace '::' in the perl module name with '-'
        modulename = m["name"].gsub("::", "-")

        # Fix any upper-casing or other naming concerns Debian has about packages
        name = "#{attributes[:cpan_package_name_prefix]}-#{modulename}"

        if m["op"].empty?
          name
        else
          # 'dep' syntax was like this (version constraint): perl(Module) > 1.0
          "#{name} (#{m["op"]})"
        end
      end
    end

    rejects = [ "perl(vars)", "perl(warnings)", "perl(strict)", "perl(Config)" ]
    self.dependencies = self.dependencies.reject do |dep|
      # Reject non-module Perl dependencies like 'vars' and 'warnings'
      rejects.include?(dep)
    end.collect(&perldepfix).collect(&method(:fix_dependency))

    # Also fix the Provides field 'perl(ModuleName) = version' to be 'perl-modulename (= version)'
    self.provides = self.provides.collect(&perldepfix).collect(&method(:fix_provides))

  end # if origin == FPM::Packagin::CPAN

  if origin == FPM::Package::Deb
    changelog_path = staging_path("usr/share/doc/#{name}/changelog.Debian.gz")
    if File.exist?(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

  if origin == FPM::Package::Deb
    changelog_path = staging_path("usr/share/doc/#{name}/changelog.gz")
    if File.exist?(changelog_path)
      logger.debug("Found an upstream changelog file, using it.", :path => changelog_path)
      attributes[:deb_upstream_changelog] = build_path("deb_upstream_changelog")
      File.open(attributes[:deb_upstream_changelog], "w") do |deb_upstream_changelog|
        Zlib::GzipReader.open(changelog_path) do |gz|
          IO::copy_stream(gz, deb_upstream_changelog)
        end
      end
      File.unlink(changelog_path)
    end
  end

  if origin == FPM::Package::Gem
    # fpm's gem input will have provides as "rubygem-name = version"
    # and we need to convert this to Debian-style "rubygem-name (= version)"
    self.provides = self.provides.collect do |provides|
      m = /^(#{attributes[:gem_package_name_prefix]})-([^\s]+)\s*=\s*(.*)$/.match(provides)
      if m
        "#{m[1]}-#{m[2]} (= #{m[3]})"
      else
        provides
      end
    end
  end
end
data_tar_flags() click to toggle source
# File lib/fpm/package/deb.rb, line 1276
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 339
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 FPM::Package#name

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

# File lib/fpm/package/deb.rb, line 294
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 498
def output(output_path)
  self.provides = self.provides.collect { |p| fix_provides(p) }

  self.provides.each do |provide|
    if !valid_provides_field?(provide)
      raise FPM::InvalidPackageConfiguration, "Found invalid Provides field values (#{provide.inspect}). This is not valid in a Debian package."
    end
  end
  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 attributes[:source_date_epoch].nil? and not attributes[:source_date_epoch_default].nil?
    attributes[:source_date_epoch] = attributes[:source_date_epoch_default]
  end
  if attributes[:source_date_epoch] == "0"
    logger.error("Alas, ruby's Zlib::GzipWriter does not support setting an mtime of zero.  Aborting.")
    raise FPM::InvalidPackageConfiguration, "#{name}: source_date_epoch of 0 not supported."
  end
  if not attributes[:source_date_epoch].nil? and not ar_cmd_deterministic?
    logger.error("Alas, could not find an ar that can handle -D option. Try installing recent gnu binutils. Aborting.")
    raise FPM::InvalidPackageConfiguration, "#{name}: ar is insufficient to support source_date_epoch."
  end
  if not attributes[:source_date_epoch].nil? and not tar_cmd_supports_sort_names_and_set_mtime?
    logger.error("Alas, could not find a tar that can set mtime and sort.  Try installing recent gnu tar. Aborting.")
    raise FPM::InvalidPackageConfiguration, "#{name}: tar is insufficient to support source_date_epoch."
  end

  attributes[:deb_systemd] = []
  attributes.fetch(:deb_systemd_list, []).each do |systemd|
    name = File.basename(systemd)
    extname = File.extname(name)

    name_with_extension = if extname.empty?
                            "#{name}.service"
                          elsif [".service", ".timer"].include?(extname)
                            name
                          else
                            raise FPM::InvalidPackageConfiguration, "Invalid systemd unit file extension: #{extname}. Expected .service or .timer, or no extension."
                          end

    dest_systemd = staging_path(File.join(attributes[:deb_systemd_path], "#{name_with_extension}"))
    mkdir_p(File.dirname(dest_systemd))
    FileUtils.cp(systemd, dest_systemd)
    File.chmod(0644, dest_systemd)

    attributes[:deb_systemd] << name_with_extension
  end

  if script?(:before_upgrade) or script?(:after_upgrade) or attributes[:deb_systemd].any?
    puts "Adding action files"
    if script?(:before_install) or script?(:before_upgrade)
      scripts[:before_install] = template("deb/preinst_upgrade.sh.erb").result(binding)
    end
    if script?(:before_remove) or not attributes[:deb_systemd].empty?
      scripts[:before_remove] = template("deb/prerm_upgrade.sh.erb").result(binding)
    end
    if script?(:after_install) or script?(:after_upgrade) or attributes[:deb_systemd].any?
      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
    if script?(:after_purge)
      scripts[:after_purge] = template("deb/postrm_upgrade.sh.erb").result(binding)
    end
  end

  # There are two changelogs that may appear:
  #   - debian-specific changelog, which should be archived as changelog.Debian.gz
  #   - upstream changelog, which should be archived as changelog.gz
  # see https://www.debian.org/doc/debian-policy/ch-docs.html#s-changelogs

  # Write the changelog.Debian.gz file
  dest_changelog = File.join(staging_path, "usr/share/doc/#{name}/changelog.Debian.gz")
  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 not attributes[:source_date_epoch].nil?
        changelog_gz.mtime = attributes[:source_date_epoch].to_i
      end
      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.

  # Write the changelog.gz file (upstream changelog)
  dest_upstream_changelog = File.join(staging_path, "usr/share/doc/#{name}/changelog.gz")
  if attributes[:deb_upstream_changelog]
    File.new(dest_upstream_changelog, "wb", 0644).tap do |changelog|
      Zlib::GzipWriter.new(changelog, Zlib::BEST_COMPRESSION).tap do |changelog_gz|
          if not attributes[:source_date_epoch].nil?
            changelog_gz.mtime = attributes[:source_date_epoch].to_i
          end
          logger.info("Writing user-specified upstream changelog", :source => attributes[:deb_upstream_changelog])
          File.new(attributes[:deb_upstream_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
      end.close
    end # No need to close, GzipWriter#close will close it.
  end

  if File.exist?(dest_changelog) and not File.exist?(dest_upstream_changelog)
    # see https://www.debian.org/doc/debian-policy/ch-docs.html#s-changelogs
    File.rename(dest_changelog, dest_upstream_changelog)
  end

  attributes.fetch(:deb_init_list, []).each do |init|
    name = File.basename(init, ".init")
    dest_init = File.join(staging_path, "etc/init.d/#{name}")
    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}")
    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_init = staging_path("etc/init.d/#{name}")
    name = "#{name}.conf" if !(name =~ /\.conf$/)
    dest_upstart = staging_path("etc/init/#{name}")
    mkdir_p(File.dirname(dest_upstart))
    FileUtils.cp(upstart, dest_upstart)
    File.chmod(0644, dest_upstart)

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

  attributes.fetch(:deb_systemd_list, []).each do |systemd|
    name = File.basename(systemd)
    extname = File.extname(systemd)
    name_with_extension = extname.empty? ? "#{name}.service" : name

    dest_systemd = staging_path(File.join(attributes[:deb_systemd_path], "#{name_with_extension}"))

    mkdir_p(File.dirname(dest_systemd))
    FileUtils.cp(systemd, dest_systemd)
    File.chmod(0644, dest_systemd)
  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")
      controltar = build_path("control.tar.gz")
      compression_flags = ["-z"]
    # gnu tar obeys GZIP environment variable with options for gzip; -n = forget original filename and date
      compressor_options = {"GZIP" => "-#{self.attributes[:deb_compression_level] || 9}" +
          "#{'n' if tar_cmd_supports_sort_names_and_set_mtime? and not attributes[:source_date_epoch].nil?}"}
    when "bzip2"
      datatar = build_path("data.tar.bz2")
      controltar = build_path("control.tar.gz")
      compression_flags = ["-j"]
      compressor_options = {"BZIP" => "-#{self.attributes[:deb_compression_level] || 9}"}
    when "xz"
      datatar = build_path("data.tar.xz")
      controltar = build_path("control.tar.xz")
      compression_flags = ["-J"]
      compressor_options = {"XZ_OPT" => "-#{self.attributes[:deb_compression_level] || 3}"}
    when "zst"
      datatar = build_path("data.tar.zst")
      controltar = build_path("control.tar.zst")
      compression_flags = ["--use-compress-program", "zstd"]
      compressor_options = {"ZSTD_CLEVEL" => "-#{self.attributes[:deb_compression_level] || 3}"}
    when "none"
      datatar = build_path("data.tar")
      controltar = build_path("control.tar")
      compression_flags = []
      compressor_options = {}
    else
      raise FPM::InvalidPackageConfiguration,
        "Unknown compression type '#{self.attributes[:deb_compression]}'"
  end
  args = [ tar_cmd, "-C", staging_path ] + compression_flags + data_tar_flags + [ "-cf", datatar, "." ]
  if tar_cmd_supports_sort_names_and_set_mtime? and not attributes[:source_date_epoch].nil?
    # Use gnu tar options to force deterministic file order and timestamp
    args += ["--sort=name", ("--mtime=@%s" % attributes[:source_date_epoch])]
  end
  args.unshift(compressor_options)
  safesystem(*args)

  # pack up the .deb, which is just an 'ar' archive with 3 files
  # the 'debian-binary' file has to be first
  File.expand_path(output_path).tap do |output_path|
    ::Dir.chdir(build_path) do
      safesystem(*ar_cmd, output_path, "debian-binary", controltar, datatar)
    end
  end

  # if a PACKAGENAME.changes file is to be created
  if self.attributes[:deb_generate_changes?]
    distribution = self.attributes[:deb_dist]

    # gather information about the files to distribute
    files = [ output_path ]
    changes_files = []
    files.each do |path|
      changes_files.push({
        :name => path,
        :size => File.size?(path),
        :md5sum => Digest::MD5.file(path).hexdigest,
        :sha1sum => Digest::SHA1.file(path).hexdigest,
        :sha256sum => Digest::SHA2.file(path).hexdigest,
      })
    end

    # write change infos to .changes file
    changes_path = File.basename(output_path, '.deb') + '.changes'
    changes_data = template("deb/deb.changes.erb").result(binding)
    File.write(changes_path, changes_data)
    logger.log("Created changes", :path => changes_path)
  end # if deb_generate_changes
end
prefix() click to toggle source
# File lib/fpm/package/deb.rb, line 320
def prefix
  return (attributes[:prefix] or "/")
end
to_s(format=nil) click to toggle source
Calls superclass method FPM::Package#to_s
# File lib/fpm/package/deb.rb, line 1270
def to_s(format=nil)
  # Default format if nil
  # git_1.7.9.3-1_amd64.deb
  return super(format.nil? ? "NAME_FULLVERSION_ARCH.EXTENSION" : format)
end
version() click to toggle source
# File lib/fpm/package/deb.rb, line 324
def version
  if @version.kind_of?(String)
    if @version.start_with?("v") && @version.gsub(/^v/, "") =~ /^#{VERSION_FIELD_PATTERN}$/
      logger.warn("Debian 'Version' field needs to start with a digit. I was provided '#{@version}' which seems like it just has a 'v' prefix to an otherwise-valid Debian version, I'll remove the 'v' for you.")
      @version = @version.gsub(/^v/, "")
    end

    if @version !~ /^#{VERSION_FIELD_PATTERN}$/
      raise FPM::InvalidPackageConfiguration, "The version looks invalid for Debian packages. Debian version field must contain only alphanumerics and . (period), + (plus), - (hyphen) or ~ (tilde). I have '#{@version}' which which isn't valid."
    end
  end

  return @version
end

Private Instance Methods

add_path(path, allconfigs) click to toggle source
# File lib/fpm/package/deb.rb, line 1106
def add_path(path, allconfigs)
  # Strip leading /
  path = path[1..-1] if path[0,1] == "/"
  cfg_path = File.expand_path(path, staging_path)
  Find.find(cfg_path) do |p|
    if File.file?(p)
      allconfigs << p.gsub("#{staging_path}/", '')
    end
  end
end
control_path(path=nil) click to toggle source
# File lib/fpm/package/deb.rb, line 985
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 848
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 468
def extract_files(package)
  # Find out the compression type
  compression = `#{ar_cmd[0]} t #{package}`.split("\n").grep(/data.tar/).first.split(".").last
  case compression
    when "gz"
      datatar = "data.tar.gz"
      compression = "-z"
    when "bzip2","bz2"
      datatar = "data.tar.bz2"
      compression = "-j"
    when "xz"
      datatar = "data.tar.xz"
      compression = "-J"
    when "zst"
      datatar = "data.tar.zst"
      compression = "--use-compress-program 'zstd -d'"
    when 'tar'
      datatar = "data.tar"
      compression = ""
    when nil
      raise FPM::InvalidPackageConfiguration, "Missing data.tar in deb source package #{package}"
    else
      raise FPM::InvalidPackageConfiguration,
        "Unknown compression type '#{compression}' for data.tar in deb source package #{package}"
  end

  # unpack the data.tar.{gz,bz2,xz} from the deb package into staging_path
  safesystem(ar_cmd[0] + " p #{package} #{datatar} | tar #{compression} -xf - -C #{staging_path}")
end
extract_info(package) click to toggle source
# File lib/fpm/package/deb.rb, line 344
def extract_info(package)
  compression = `#{ar_cmd[0]} t #{package}`.split("\n").grep(/control.tar/).first.split(".").last
  case compression
    when "gz"
      controltar = "control.tar.gz"
      compression = "-z"
    when "bzip2","bz2"
      controltar = "control.tar.bz2"
      compression = "-j"
    when "xz"
      controltar = "control.tar.xz"
      compression = "-J"
    when "zst"
      controltar = "control.tar.zst"
      compression = "--use-compress-program 'zstd -d'"
    when 'tar'
      controltar = "control.tar"
      compression = ""
    when nil
      raise FPM::InvalidPackageConfiguration, "Missing control.tar in deb source package #{package}"
    else
      raise FPM::InvalidPackageConfiguration,
        "Unknown compression type '#{compression}' for control.tar in deb source package #{package}"
  end

  build_path("control").tap do |path|
    FileUtils.mkdir(path) if !File.directory?(path)
    # unpack the control.tar.{,gz,bz2,xz,zst} from the deb package into staging_path
    # Unpack the control tarball
    safesystem(ar_cmd[0] + " p #{package} #{controltar} | tar #{compression} -xf - -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 FPM::InvalidPackageConfiguration, "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
    parse.call("Provides").tap 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: .*/m].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?]

    if File.file?(File.join(path, "preinst"))
      self.scripts[:before_install] = File.read(File.join(path, "preinst"))
    end
    if File.file?(File.join(path, "postinst"))
      self.scripts[:after_install] = File.read(File.join(path, "postinst"))
    end
    if File.file?(File.join(path, "prerm"))
      self.scripts[:before_remove] = File.read(File.join(path, "prerm"))
    end
    if File.file?(File.join(path, "postrm"))
      self.scripts[:after_remove] = File.read(File.join(path, "postrm"))
    end
    if File.file?(File.join(path, "conffiles"))
      self.config_files = File.read(File.join(path, "conffiles")).split("\n")
    end
  end
end
fix_dependency(dep) click to toggle source
# File lib/fpm/package/deb.rb, line 854
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

  if dep.start_with?("/")
    logger.warn("Blanking 'dependency' field '#{dep}' because it's invalid")
    dep = ""
    return dep
  end

  if dep.include?("rpmlib")
    logger.warn("Blanking 'dependency' field '#{dep}' because it's invalid")
    dep = ""
    return dep
  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
    if l > 1
      nextversion[l-2] += 1
      nextversion[l-1] = 0
    else
      # Single component versions ~> 1
      nextversion[l-1] += 1
    end
    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 958
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

  if provides.include?("(") and !provides.include?("(=")
    logger.warn("Blanking 'provides' field '#{provides}' because it's invalid")
    provides = ""
  end

  if m = provides.match(/^([A-Za-z0-9_-]+)\s*=\s*(\d+.*$)/)
    logger.warn("Replacing 'provides' entry #{provides} with syntax 'name (= version)'")
    provides = "#{m[1]} (= #{m[2]})"
  end
  return provides.rstrip
end
mkdir_p(dir) click to toggle source
# File lib/fpm/package/deb.rb, line 1266
def mkdir_p(dir)
  FileUtils.mkdir_p(dir, :mode => 0755)
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 443
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
valid_provides_field?(text) click to toggle source
# File lib/fpm/package/deb.rb, line 932
def valid_provides_field?(text)
  m = RELATIONSHIP_FIELD_PATTERN.match(text)
  if m.nil?
    logger.error("Invalid relationship field for debian package: #{text}")
    return false
  end

  # Per Debian Policy manual, https://www.debian.org/doc/debian-policy/ch-relationships.html#syntax-of-relationship-fields
  # >> The relations allowed are <<, <=, =, >= and >> for strictly earlier, earlier or equal,
  # >> exactly equal, later or equal and strictly later, respectively. The exception is the
  # >> Provides field, for which only = is allowed
  if m["relation"] == "=" || m["relation"] == nil
    return true
  end
  return false
end
valid_relationship_field?(text) click to toggle source
# File lib/fpm/package/deb.rb, line 949
def valid_relationship_field?(text)
  m = RELATIONSHIP_FIELD_PATTERN.match(text)
  if m.nil?
    logger.error("Invalid relationship field for debian package: #{text}")
    return false
  end
  return true
end
write_conffiles() click to toggle source
# File lib/fpm/package/deb.rb, line 1104
def write_conffiles
  # expand recursively a given path to be put in allconfigs
  def add_path(path, allconfigs)
    # Strip leading /
    path = path[1..-1] if path[0,1] == "/"
    cfg_path = File.expand_path(path, staging_path)
    Find.find(cfg_path) do |p|
      if File.file?(p)
        allconfigs << p.gsub("#{staging_path}/", '')
      end
    end
  end

  # check for any init scripts or default files
  inits    = attributes.fetch(:deb_init_list, [])
  defaults = attributes.fetch(:deb_default_list, [])
  upstarts = attributes.fetch(:deb_upstart_list, [])
  etcfiles = []
  # Add everything in /etc
  begin
    if !attributes[:deb_no_default_config_files?] && File.exist?(staging_path("/etc"))
      logger.warn("Debian packaging tools generally labels all files in /etc as config files, " \
                  "as mandated by policy, so fpm defaults to this behavior for deb packages. " \
                  "You can disable this default behavior with --deb-no-default-config-files flag")
      add_path("/etc", etcfiles)
    end
  rescue Errno::ENOENT
  end

  return unless (config_files.any? or inits.any? or defaults.any? or upstarts.any? or etcfiles.any?)

  allconfigs = etcfiles

  # scan all conf file paths for files and add them
  config_files.each do |path|
    logger.debug("Checking if #{path} exists")
    cfe = File.exist?("#{path}")
    logger.debug("Check result #{cfe}")
    begin
      add_path(path, allconfigs)
    rescue Errno::ENOENT
      if !cfe
        raise FPM::InvalidPackageConfiguration,
          "Error trying to use '#{path}' as a config file in the package. Does it exist?"
      else
        dcl = File.join(staging_path, path)
        if !File.exist?("#{dcl}")
          logger.debug("Adding config file #{path} to Staging area #{staging_path}")
          FileUtils.mkdir_p(File.dirname(dcl))
          FileUtils.cp_r path, dcl
        else
          logger.debug("Config file aready exists in staging area.")
        end
      end
    end
  end

  if attributes[:deb_auto_config_files?]
    inits.each do |init|
      name = File.basename(init, ".init")
      initscript = "/etc/init.d/#{name}"
      logger.debug("Add conf file declaration for init script", :script => initscript)
      allconfigs << initscript[1..-1]
    end
    defaults.each do |default|
      name = File.basename(default, ".default")
      confdefaults = "/etc/default/#{name}"
      logger.debug("Add conf file declaration for defaults", :default => confdefaults)
      allconfigs << confdefaults[1..-1]
    end
    upstarts.each do |upstart|
      name = File.basename(upstart, ".upstart")
      upstartscript = "etc/init/#{name}.conf"
      logger.debug("Add conf file declaration for upstart script", :script => upstartscript)
      allconfigs << upstartscript[1..-1]
    end
  end

  allconfigs.sort!.uniq!
  return unless allconfigs.any?

  control_path("conffiles").tap do |conffiles|
    File.open(conffiles, "w") do |out|
      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 1051
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
  control_path("control").tap 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 996
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

  # Tar up the staging_path into control.tar.{compression type}
  case self.attributes[:deb_compression]
    when "gz", "bzip2", nil
      controltar = "control.tar.gz"
      compression_flags = ["-z"]
      # gnu tar obeys GZIP environment variable with options for gzip; -n = forget original filename and date
      compressor_options = {"GZIP" => "-#{self.attributes[:deb_compression_level] || 9}" +
          "#{'n' if tar_cmd_supports_sort_names_and_set_mtime? and not attributes[:source_date_epoch].nil?}"}
    when "xz"
      controltar = "control.tar.xz"
      compression_flags = ["-J"]
      compressor_options = {"XZ_OPT" => "-#{self.attributes[:deb_compression_level] || 3}"}
    when "zst"
      controltar = "control.tar.zst"
      compression_flags = ["--use-compress-program", "zstd"]
      compressor_options = {"ZSTD_CLEVEL" => "-#{self.attributes[:deb_compression_level] || 3}"}
    when "none"
      controltar = "control.tar"
      compression_flags = []
      compressor_options = {}
    else
      raise FPM::InvalidPackageConfiguration,
        "Unknown compression type '#{self.attributes[:deb_compression]}'"
  end

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

    args = [ tar_cmd, "-C", control_path ] + compression_flags + [ "-cf", controltar,
      "--owner=0", "--group=0", "--numeric-owner", "." ]
    if tar_cmd_supports_sort_names_and_set_mtime? and not attributes[:source_date_epoch].nil?
      # Force deterministic file order and timestamp
      args += ["--sort=name", ("--mtime=@%s" % attributes[:source_date_epoch])]
    end
    args.unshift(compressor_options)
    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 1205
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(0644, control_path("templates"))
  end
end
write_md5sums() click to toggle source
# File lib/fpm/package/deb.rb, line 1245
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 1217
def write_meta_files
  files = attributes[:deb_meta_file]
  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 1091
def write_scripts
  SCRIPT_MAP.each do |scriptname, filename|
    next unless script?(scriptname)

    control_path(filename).tap 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 1196
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
  File.chmod(0644, control_path("shlibs"))
end
write_triggers() click to toggle source
# File lib/fpm/package/deb.rb, line 1227
def write_triggers
  lines = [['interest', :deb_interest],
           ['activate', :deb_activate],
           ['interest-noawait', :deb_interest_noawait],
           ['activate-noawait', :deb_activate_noawait],
           ].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.chmod 0644
      f.write "\n" if f.size > 0
      f.write lines
    end
  end
end