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)
- SCRIPT_MAP
Map of what scripts are named.
Public Class Methods
FPM::Package::new
# File lib/fpm/package/deb.rb, line 163 def initialize(*args) super(*args) attributes[:deb_priority] = "extra" end
Public Instance Methods
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 173 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
# File lib/fpm/package/deb.rb, line 517 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
# File lib/fpm/package/deb.rb, line 864 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
# File lib/fpm/package/deb.rb, line 233 def input(input_path) extract_info(input_path) extract_files(input_path) end
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 203 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
# File lib/fpm/package/deb.rb, line 361 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 attributes.fetch(:deb_systemd_list, []).each do |systemd| name = File.basename(systemd, ".service") dest_systemd = staging_path("lib/systemd/system/#{name}.service") FileUtils.mkdir_p(File.dirname(dest_systemd)) FileUtils.cp(systemd, dest_systemd) File.chmod(0644, dest_systemd) # set the attribute with the systemd service name attributes[:deb_systemd] = name end if script?(:before_upgrade) or script?(:after_upgrade) or attributes[:deb_systemd] 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 attributes[:deb_systemd] scripts[:before_remove] = template("deb/prerm_upgrade.sh.erb").result(binding) end if script?(:after_install) or script?(:after_upgrade) or attributes[:deb_systemd] 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 attributes.fetch(:deb_systemd_list, []).each do |systemd| name = File.basename(systemd, ".service") dest_systemd = staging_path("lib/systemd/system/#{name}.service") FileUtils.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") 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 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 File.expand_path(output_path).tap do |output_path| ::Dir.chdir(build_path) do safesystem("ar", "-qc", output_path, "debian-binary", "control.tar.gz", datatar) end end end
# File lib/fpm/package/deb.rb, line 229 def prefix return (attributes[:prefix] or "/") end
FPM::Package#to_s
# File lib/fpm/package/deb.rb, line 857 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
expand recursively a given path to be put in allconfigs
# File lib/fpm/package/deb.rb, line 723 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
# File lib/fpm/package/deb.rb, line 624 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
# File lib/fpm/package/deb.rb, line 540 def debianize_op(op) # Operators in debian packaging are <<, <=, =, >= and >> # So any operator like < or > must be replaced {:< => "<<", :> => ">>"}[op.to_sym] or op end
# File lib/fpm/package/deb.rb, line 337 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
# File lib/fpm/package/deb.rb, line 238 def extract_info(package) build_path("control").tap 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 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
# File lib/fpm/package/deb.rb, line 546 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
# File lib/fpm/package/deb.rb, line 607 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 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 312 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
# File lib/fpm/package/deb.rb, line 713 def write_conffiles # 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, []) return unless (config_files.any? or inits.any? or defaults.any? or upstarts.any?) allconfigs = [] # 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 # scan all conf file paths for files and add them config_files.each do |path| begin add_path(path, allconfigs) rescue Errno::ENOENT raise FPM::InvalidPackageConfiguration, "Error trying to use '#{path}' as a config file in the package. Does it exist?" end end # Also add everything in /etc begin if !attributes[:deb_no_default_config_files?] 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", allconfigs) end rescue Errno::ENOENT 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
# File lib/fpm/package/deb.rb, line 660 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
# File lib/fpm/package/deb.rb, line 635 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 build_path("control.tar.gz").tap 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
# File lib/fpm/package/deb.rb, line 799 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
# File lib/fpm/package/deb.rb, line 836 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
# File lib/fpm/package/deb.rb, line 811 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 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 700 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
# File lib/fpm/package/deb.rb, line 790 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
# File lib/fpm/package/deb.rb, line 821 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.chmod 0644 f.write "\n" if f.size > 0 f.write lines end end end