1
0
mirror of https://github.com/jordansissel/fpm synced 2025-09-01 04:30:48 +02:00
fpm/lib/fpm/package.rb
Jordan Sissel 8f2dd4516a Default :workdir attribute to Dir.tmpdir
This fixes a bug where rpm output passes an rpm build macro `_tmppath`
that is empty, and on Fedora 27 this causes rpmbuild to fail.

The test suite catches this by making most all RPM specs fail, and with
this change, most of them now pass.
2018-01-10 21:09:17 -08:00

560 lines
18 KiB
Ruby

require "fpm/namespace" # local
require "fpm/util" # local
require "pathname" # stdlib
require "find"
require "tmpdir" # stdlib
require "ostruct"
require "backports/2.0.0/stdlib/ostruct"
require "socket" # stdlib, for Socket.gethostname
require "shellwords" # stdlib, for Shellwords.escape
require "erb" # stdlib, for template processing
require "cabin" # gem "cabin"
require "stud/temporary"
# This class is the parent of all packages.
# If you want to implement an FPM package type, you'll inherit from this.
class FPM::Package
include FPM::Util
include Cabin::Inspectable
# This class is raised if there's something wrong with a setting in the package.
class InvalidArgument < StandardError; end
# This class is raised when a file already exists when trying to write.
class FileAlreadyExists < StandardError
# Get a human-readable error message
def to_s
return "File already exists, refusing to continue: #{super}"
end # def to_s
end # class FileAlreadyExists
# This class is raised when you try to output a package to a path
# whose containing directory does not exist.
class ParentDirectoryMissing < StandardError
def to_s
return "Parent directory does not exist: #{File.dirname(super)} - cannot write to #{super}"
end # def to_s
end # class ParentDirectoryMissing
# The name of this package
attr_accessor :name
# The version of this package (the upstream version)
attr_accessor :version
# The epoch version of this package
# This is used most when an upstream package changes it's versioning
# style so standard comparisions wouldn't work.
attr_accessor :epoch
# The iteration of this package.
# Debian calls this 'release' and is the last '-NUMBER' in the version
# RedHat has this as 'Release' in the .spec file
# FreeBSD calls this 'PORTREVISION'
#
# Iteration can be nil. If nil, the fpm package implementation is expected
# to handle any default value that should be instead.
attr_accessor :iteration
# Who maintains this package? This could be the upstream author
# or the package maintainer. You pick.
attr_accessor :maintainer
# A identifier representing the vendor. Any string is fine.
# This is usually who produced the software.
attr_accessor :vendor
# URL for this package.
# Could be the homepage. Could be the download url. You pick.
attr_accessor :url
# The category of this package.
# RedHat calls this 'Group'
# Debian calls this 'Section'
# FreeBSD would put this in /usr/ports/<category>/...
attr_accessor :category
# A identifier representing the license. Any string is fine.
attr_accessor :license
# What architecture is this package for?
attr_accessor :architecture
# Array of dependencies.
attr_accessor :dependencies
# Array of things this package provides.
# (Not all packages support this)
attr_accessor :provides
# Array of things this package conflicts with.
# (Not all packages support this)
attr_accessor :conflicts
# Array of things this package replaces.
# (Not all packages support this)
attr_accessor :replaces
# a summary or description of the package
attr_accessor :description
# hash of scripts for maintainer/package scripts (postinstall, etc)
#
# The keys are :before_install, etc
# The values are the text to use in the script.
attr_accessor :scripts
# Array of configuration files
attr_accessor :config_files
attr_accessor :directories
# Any other attributes specific to this package.
# This is where you'd put rpm, deb, or other specific attributes.
attr_accessor :attributes
attr_accessor :attrs
private
def initialize
# Attributes for this specific package
@attributes = {
# Default work location
:workdir => ::Dir.tmpdir
}
# Reference
# http://www.debian.org/doc/manuals/maint-guide/first.en.html
# http://wiki.debian.org/DeveloperConfiguration
# https://github.com/jordansissel/fpm/issues/37
if ENV.include?("DEBEMAIL") and ENV.include?("DEBFULLNAME")
# Use DEBEMAIL and DEBFULLNAME as the default maintainer if available.
@maintainer = "#{ENV["DEBFULLNAME"]} <#{ENV["DEBEMAIL"]}>"
else
# TODO(sissel): Maybe support using 'git config' for a default as well?
# git config --get user.name, etc can be useful.
#
# Otherwise default to user@currenthost
@maintainer = "<#{ENV["USER"]}@#{Socket.gethostname}>"
end
# Set attribute defaults based on flags
# This allows you to define command line options with default values
# that also are obeyed if fpm is used programmatically.
self.class.default_attributes do |attribute, value|
attributes[attribute] = value
end
@name = nil
@architecture = "native"
@description = "no description given"
@version = nil
@epoch = nil
@iteration = nil
@url = nil
@category = "default"
@license = "unknown"
@vendor = "none"
# Iterate over all the options and set defaults
if self.class.respond_to?(:declared_options)
self.class.declared_options.each do |option|
option.attribute_name.tap do |attr|
# clamp makes option attributes available as accessor methods
# do --foo-bar is available as 'foo_bar'
# make these available as package attributes.
attr = "#{attr}?" if !respond_to?(attr)
input.attributes[attr.to_sym] = send(attr) if respond_to?(attr)
end
end
end
@provides = []
@conflicts = []
@replaces = []
@dependencies = []
@scripts = {}
@config_files = []
@directories = []
@attrs = {}
build_path
# Dont' initialize staging_path just yet, do it lazily so subclass can get a word in.
end # def initialize
# Get the 'type' for this instance.
#
# For FPM::Package::ABC, this returns 'abc'
def type
self.class.type
end # def type
# Convert this package to a new package type
def convert(klass)
logger.info("Converting #{self.type} to #{klass.type}")
exclude
pkg = klass.new
pkg.cleanup_staging # purge any directories that may have been created by klass.new
# copy other bits
ivars = [
:@architecture, :@category, :@config_files, :@conflicts,
:@dependencies, :@description, :@epoch, :@iteration, :@license, :@maintainer,
:@name, :@provides, :@replaces, :@scripts, :@url, :@vendor, :@version,
:@directories, :@staging_path, :@attrs
]
ivars.each do |ivar|
#logger.debug("Copying ivar", :ivar => ivar, :value => instance_variable_get(ivar),
#:from => self.type, :to => pkg.type)
pkg.instance_variable_set(ivar, instance_variable_get(ivar))
end
# Attributes are special! We do not want to remove the default values of
# the destination package type unless their value is specified on the
# source package object.
pkg.attributes.merge!(self.attributes)
pkg.converted_from(self.class)
return pkg
end # def convert
# This method is invoked on a package when it has been converted to a new
# package format. The purpose of this method is to do any extra conversion
# steps, like translating dependency conditions, etc.
def converted_from(origin)
# nothing to do by default. Subclasses may implement this.
# See the RPM package class for an example.
end # def converted
# Add a new source to this package.
# The exact behavior depends on the kind of package being managed.
#
# For instance:
#
# * for FPM::Package::Dir, << expects a path to a directory or files.
# * for FPM::Package::RPM, << expects a path to an rpm.
#
# The idea is that you can keep pumping in new things to a package
# for later conversion or output.
#
# Implementations are expected to put files relevant to the 'input' in the
# staging_path
def input(thing_to_input)
raise NotImplementedError.new("#{self.class.name} does not yet support " \
"reading #{self.type} packages")
end # def input
# Output this package to the given path.
def output(path)
raise NotImplementedError.new("#{self.class.name} does not yet support " \
"creating #{self.type} packages")
end # def output
def staging_path(path=nil)
@staging_path ||= Stud::Temporary.directory("package-#{type}-staging")
if path.nil?
return @staging_path
else
return File.join(@staging_path, path)
end
end # def staging_path
def build_path(path=nil)
@build_path ||= Stud::Temporary.directory("package-#{type}-build")
if path.nil?
return @build_path
else
return File.join(@build_path, path)
end
end # def build_path
# Clean up any temporary storage used by this class.
def cleanup
cleanup_staging
cleanup_build
end # def cleanup
def cleanup_staging
if File.directory?(staging_path)
logger.debug("Cleaning up staging path", :path => staging_path)
FileUtils.rm_r(staging_path)
end
end # def cleanup_staging
def cleanup_build
if File.directory?(build_path)
logger.debug("Cleaning up build path", :path => build_path)
FileUtils.rm_r(build_path)
end
end # def cleanup_build
# List all files in the staging_path
#
# The paths will all be relative to staging_path and will not include that
# path.
#
# This method will emit 'leaf' paths. Files, symlinks, and other file-like
# things are emitted. Intermediate directories are ignored, but
# empty directories are emitted.
def files
is_leaf = lambda do |path|
# True if this is a file/symlink/etc, but not a plain directory
return true if !(File.directory?(path) and !File.symlink?(path))
# Empty directories are leafs as well.
return true if ::Dir.entries(path).sort == [".", ".."]
# False otherwise (non-empty directory, etc)
return false
end # is_leaf
# Find all leaf-like paths (files, symlink, empty directories, etc)
# Also trim the leading path such that '#{staging_path}/' is removed from
# the path before returning.
#
# Wrapping Find.find in an Enumerator is required for sane operation in ruby 1.8.7,
# but requires the 'backports' gem (which is used in other places in fpm)
return Enumerator.new { |y| Find.find(staging_path) { |path| y << path } } \
.select { |path| path != staging_path } \
.select { |path| is_leaf.call(path) } \
.collect { |path| path[staging_path.length + 1.. -1] }
end # def files
def template_dir
File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "templates"))
end
def template(path)
template_path = File.join(template_dir, path)
template_code = File.read(template_path)
logger.info("Reading template", :path => template_path)
erb = ERB.new(template_code, nil, "-")
erb.filename = template_path
return erb
end # def template
#######################################
# The following methods are provided to
# easily override particular substitut-
# ions performed by to_s for subclasses
#######################################
def to_s_arch; architecture.to_s; end
def to_s_name; name.to_s; end
def to_s_fullversion; iteration ? "#{version}-#{iteration}" : "#{version}"; end
def to_s_version; version.to_s; end
def to_s_iteration; iteration.to_s; end
def to_s_epoch; epoch.to_s; end
def to_s_type; type.to_s; end
def to_s_extension; type.to_s; end
#######################################
def to_s(fmt=nil)
fmt = "NAME.EXTENSION" if fmt.nil?
return fmt.gsub("ARCH", to_s_arch) \
.gsub("NAME", to_s_name) \
.gsub("FULLVERSION", to_s_fullversion) \
.gsub("VERSION", to_s_version) \
.gsub("ITERATION", to_s_iteration) \
.gsub("EPOCH", to_s_epoch) \
.gsub("TYPE", to_s_type) \
.gsub("EXTENSION", to_s_extension)
end # def to_s
def edit_file(path)
editor = ENV['FPM_EDITOR'] || ENV['EDITOR'] || 'vi'
logger.info("Launching editor", :file => path)
command = "#{editor} #{Shellwords.escape(path)}"
system("#{editor} #{Shellwords.escape(path)}")
if !$?.success?
raise ProcessFailed.new("'#{editor}' failed (exit code " \
"#{$?.exitstatus}) Full command was: " \
"#{command}");
end
if File.size(path) == 0
raise "Empty file after editing: #{path.inspect}"
end
end # def edit_file
# This method removes excluded files from the staging_path. Subclasses can
# remove the files during the input phase rather than deleting them here
def exclude
return if attributes[:excludes].nil?
if @attributes.include?(:prefix)
installdir = staging_path(@attributes[:prefix])
else
installdir = staging_path
end
Find.find(installdir) do |path|
match_path = path.sub("#{installdir.chomp('/')}/", '')
attributes[:excludes].each do |wildcard|
logger.debug("Checking path against wildcard", :path => match_path, :wildcard => wildcard)
if File.fnmatch(wildcard, match_path)
logger.info("Removing excluded path", :path => match_path, :matches => wildcard)
FileUtils.rm_r(path)
Find.prune
break
end
end
end
end # def exclude
class << self
# This method is invoked when subclass occurs.
#
# Lets us track all known FPM::Package subclasses
def inherited(klass)
@subclasses ||= {}
@subclasses[klass.name.gsub(/.*:/, "").downcase] = klass
end # def self.inherited
# Get a list of all known package subclasses
def types
return @subclasses
end # def self.types
# This allows packages to define flags for the fpm command line
def option(flag, param, help, options={}, &block)
@options ||= []
if !flag.is_a?(Array)
flag = [flag]
end
if param == :flag
# Automatically make 'flag' (boolean) options tunable with '--[no-]...'
flag = flag.collect { |f| "--[no-]#{type}-#{f.gsub(/^--/, "")}" }
else
flag = flag.collect { |f| "--#{type}-#{f.gsub(/^--/, "")}" }
end
help = "(#{type} only) #{help}"
@options << [flag, param, help, options, block]
end # def options
# Apply the options for this package on the clamp command
#
# Package flags become attributes '{type}-flag'
#
# So if you have:
#
# class Foo < FPM::Package
# option "--bar-baz" ...
# end
#
# The attribute value for --foo-bar-baz will be :foo_bar_baz"
def apply_options(clampcommand)
@options ||= []
@options.each do |args|
flag, param, help, options, block = args
clampcommand.option(flag, param, help, options, &block)
end
end # def apply_options
def default_attributes(&block)
return if @options.nil?
@options.each do |flag, param, help, options, _block|
attr = flag.first.gsub(/^-+/, "").gsub(/-/, "_").gsub("[no_]", "")
attr += "?" if param == :flag
yield attr.to_sym, options[:default]
end
end # def default_attributes
# Get the type of this package class.
#
# For "Foo::Bar::BAZ" this will return "baz"
def type
self.name.split(':').last.downcase
end # def self.type
end # class << self
# Get the version of this package
def version
if instance_variable_defined?(:@version) && !@version.nil?
return @version
elsif attributes[:version_given?]
# 'version_given?' will be true in cases where the
# fpm command-line tool has been given '-v' or '--version' settings
# We do this check because the default version is "1.0"
# on the fpm command line.
return attributes.fetch(:version)
end
# No version yet, nil.
return nil
end # def version
# Does this package have the given script?
def script?(name)
return scripts.include?(name)
end # def script?
# write all scripts to .scripts (tar and dir)
def write_scripts
scripts_path = File.join(staging_path, ".scripts")
target_scripts = [:before_install, :after_install, :before_remove, :after_remove]
if target_scripts.any? {|name| script?(name)}
::Dir.mkdir(scripts_path)
target_scripts.each do |name|
next unless script?(name)
out = File.join(scripts_path, name.to_s)
logger.debug('Writing script', :source => name, :target => out)
File.write(out, script(name))
File.chmod(0755, out)
end
end
end
# Get the contents of the script by a given name.
#
# If template_scripts? is set in attributes (often by the --template-scripts
# flag), then apply it as an ERB template.
def script(script_name)
if attributes[:template_scripts?]
erb = ERB.new(scripts[script_name], nil, "-")
# TODO(sissel): find the original file name for the file.
erb.filename = "script(#{script_name})"
return erb.result(binding)
else
return scripts[script_name]
end
end # def script
def output_check(output_path)
if !File.directory?(File.dirname(output_path))
raise ParentDirectoryMissing.new(output_path)
end
if File.file?(output_path)
if attributes[:force?]
logger.warn("Force flag given. Overwriting package at #{output_path}")
File.delete(output_path)
else
raise FileAlreadyExists.new(output_path)
end
end
end # def output_path
def provides=(value)
if !value.is_a?(Array)
@provides = [value]
else
@provides = value
end
end
# General public API
public(:type, :initialize, :convert, :input, :output, :to_s, :cleanup, :files,
:version, :script, :provides=)
# Package internal public api
public(:cleanup_staging, :cleanup_build, :staging_path, :converted_from,
:edit_file, :build_path)
end # class FPM::Package