Cross package maker. DEB/RPM generation or conversion. Derived from jordansissel/fpm.

⌈⌋ ⎇ branch:  cross package maker


Artifact [2bead5e67d]

Artifact 2bead5e67d048c462b4f0e9596cb04df0505d5b2:

  • File lib/fpm/command.rb — part of check-in [160c9333a7] at 2012-03-19 07:17:25 on branch trunk — Remove unused and mysterious 'settings' method (user: jls@semicomplete.com size: 13466)

require "rubygems"
require "fpm/namespace"
require "fpm/util"
require "clamp"
require "ostruct"
require "fpm"

if $DEBUG
  Cabin::Channel.get(Kernel).subscribe($stdout)
  Cabin::Channel.get(Kernel).level = :debug
end

Dir[File.join(File.dirname(__FILE__), "package", "*.rb")].each do |plugin|
  Cabin::Channel.get(Kernel).info("Loading plugin", :path => plugin)
  require plugin
end


# The main fpm command entry point.
class FPM::Command < Clamp::Command
  include FPM::Util

  option "-t", "OUTPUT_TYPE",
    "the type of package you want to create (deb, rpm, solaris, etc)",
    :attribute_name => :output_type
  option "-s", "INPUT_TYPE",
    "the package type to use as input (gem, rpm, python, etc)",
    :attribute_name => :input_type
  option "-C", "CHDIR",
    "Change directory to here before searching for files",
    :attribute_name => :chdir
  option "--prefix", "PREFIX",
    "A path to prefix files with when building the target package. This may " \
    "be necessary for all input packages. For example, the 'gem' type will" \
    "prefix with your gem directory automatically."
  option ["-p", "--package"], "OUTPUT", "The package file path to output."
  option ["-n", "--name"], "NAME", "The name to give to the package"
  option "--verbose", :flag, "Enable verbose output"
  option "--debug", :flag, "Enable debug output"
  option ["-v", "--version"], "VERSION", "The version to give to the package",
    :default => 1.0
  option "--iteration", "ITERATION",
    "The iteration to give to the package. RPM calls this the 'release'. " \
    "FreeBSD calls it 'PORTREVISION'. Debian calls this 'debian_revision'",
    :default => "1"
  option "--epoch", "EPOCH",
    "The epoch value for this package. RPM and Debian calls this 'epoch'. " \
    "FreeBSD calls this 'PORTEPOCH'", :default => "1"
  option "--license", "LICENSE",
    "(optional) license name for this package"
  option "--vendor", "VENDOR",
    "(optional) vendor name for this package"
  option "--category", "CATEGORY",
    "(optional) category this package belongs to", :default => "none"
  option ["-d", "--depends"], "DEPENDENCY",
    "A dependency. This flag can be specified multiple times. Value is " \
    "usually in the form of: -d 'name' or -d 'name > version'",
    :default => [], :attribute_name => :dependencies do |val|
    # Clamp doesn't support multivalue flags (ie; specifying -d multiple times)
    # so we can hack around it with this trickery.
    @dependencies ||= []
    @dependencies << val
  end # -d / --depends
  option "--provides", "PROVIDES",
    "What this package provides (usually a name)" do |val|
    @provides ||= []
    @provides << val
  end # --provides
  option "--conflicts", "CONFLICTS",
    "Other packages/versions this package conflicts with" do |val|
    @conflicts ||= []
    @conflicts << val
  end # --conflicts
  option "--replaces", "REPLACES",
    "Other packages/versions this package replaces" do |val|
    @replaces ||= []
    @replaces << val
  end # --replaces
  option "--config-files", "CONFIG_FILES",
    "Mark a file in the package as being a config file. This uses 'conffiles'" \
    " in debs and %config in rpm. You can specify a directory to have it " \
    "scanned marking all files found as config files" do |val|
    @config_files ||= []
    @config_files << val
  end # --config-files
  option ["-a", "--architecture"], "ARCHITECTURE",
    "The architecture name. Usually matches 'uname -m'. For automatic values," \
    " you can use '-a all' or '-a native'. These two strings will be " \
    "translated into the correct value for your platform and target package type."
  option ["-m", "--maintainer"], "MAINTAINER",
    "The maintainer of this package.",
    :default => "<#{ENV["USER"]}@#{Socket.gethostname}>"
  option ["-S", "--package-name-suffix"], "PACKAGE_NAME_SUFFIX",
    "a name suffix to append to package and dependencies."
  option ["-e", "--edit"], :flag,
    "Edit the package spec before building.", :default => false
  option ["-x", "--exclude"], "EXCLUDE_PATTERN",
    "Exclude paths matching pattern (shell wildcard globs valid here)" do |val|
    @exclude_pattern ||= []
    @exclude_pattern << val
  end # -x / --exclude
  option "--description", "DESCRIPTION", "Add a description for this package.",
    :default => "no description"
  option "--url", "URI", "Add a url for this package.",
    :default => "http://example.com/no-uri-given"
  option "--inputs", "INPUTS_PATH",
    "The path to a file containing a newline-separated list of " \
    "files and dirs to use as input."

  option "--post-install", "FILE",
    "(DEPRECATED, use --after-install) a script to be run after " \
    "package installation" do |val|
    @after_install = File.expand_path(val) # Get the full path to the script
  end # --post-install (DEPRECATED)
  option "--pre-install", "FILE",
    "(DEPRECATED, use --before-install) a script to be run before " \
    "package installation" do |val|
    @before_install = File.expand_path(val) # Get the full path to the script
  end # --pre-install (DEPRECATED)
  option "--post-uninstall", "FILE",
      "(DEPRECATED, use --after-remove) a script to be run after " \
      "package removal" do |val|
    @after_remove = File.expand_path(val) # Get the full path to the script
  end # --post-uninstall (DEPRECATED)
  option "--pre-uninstall", "FILE",
    "(DEPRECATED, use --before-remove) a script to be run before " \
    "package removal"  do |val|
    @before_remove = File.expand_path(val) # Get the full path to the script
  end # --pre-uninstall (DEPRECATED)

  option "--after-install", "FILE",
    "a script to be run after package installation" do |val|
    File.expand_path(val) # Get the full path to the script
  end # --after-install
  option "--before-install", "FILE",
    "a script to be run before package installation" do |val|
    File.expand_path(val) # Get the full path to the script
  end # --pre-install
  option "--after-remove", "FILE",
    "a script to be run after package removal" do |val|
    File.expand_path(val) # Get the full path to the script
  end # --after-remove
  option "--before-remove", "FILE",
    "a script to be run before package removal" do |val|
    File.expand_path(val) # Get the full path to the script
  end # --before-remove

  parameter "[ARGS] ...",
    "Inputs to the source package type. For the 'dir' type, this is the files" \
    " and directories you want to include in the package. For others, like " \
    "'gem', it specifies the packages to download and use as the gem input",
    :attribute_name => :args

  FPM::Package.types.each do |name, klass|
    klass.apply_options(self)
  end

  # TODO(sissel): expose 'option' and 'parameter' junk to FPM::Package and subclasses.
  # Apply those things to this command.
  #
  # Add extra flags from plugins
  #FPM::Package::Gem.flags(FPM::Flags.new(opts, "gem", "gem only"), @settings)
  #FPM::Package::Python.flags(FPM::Flags.new(opts, "python", "python only"),
                            #@settings)
  #FPM::Package::Deb.flags(FPM::Flags.new(opts, "deb", "deb only"), @settings)
  #FPM::Package::Rpm.flags(FPM::Flags.new(opts, "rpm", "rpm only"), @settings)
  
  # A new FPM::Command
  def initialize(*args)
    super(*args)
    @conflicts = []
    @replaces = []
    @provides = []
    @dependencies = []
    @config_files = []
  end # def initialize

  # Execute this command. See Clamp::Command#execute and Clamp's documentation
  def execute
    @logger = Cabin::Channel.get
    @logger.subscribe(STDOUT)
    @logger.level = :warn

    validator = Validator.new(self)
    if !validator.ok?
      validator.messages.each do |message|
        @logger.warn(message)
      end

      @logger.fatal("Fix the above problems, and you'll be rolling packages in no time!")
      return 1
    end
    input_class = FPM::Package.types[input_type]
    output_class = FPM::Package.types[output_type]

    @logger.level = :info if verbose? # --verbose
    @logger.level = :debug if debug? # --debug

    input = input_class.new

    # Merge in package settings. 
    # The 'settings' stuff comes in from #apply_options, which goes through
    # all the options defined in known packages and puts them into our command.
    # Flags in packages defined as "--foo-bar" become named "--<packagetype>-foo-bar"
    # They are stored in 'settings' as :gem_foo_bar.
    input.attributes ||= {}

    # Iterate over all the options
    self.class.declared_options.each do |option|
      with(option.attribute_name) 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

    args.each do |arg| 
      input.input(arg) 
    end

    # If --inputs was specified, read it as a file.
    if !inputs.nil?
      if !File.exists?(inputs)
        @logger.fatal("File given for --inputs does not exist (#{inputs})")
        return 1
      end

      # Read each line as a path
      File.new(inputs, "r").each_line { |path| input.input(path) }
    end

    # Override package settings if they are not the default flag values
    # the below proc essentially does:
    #
    # if someflag != default_someflag
    #   input.someflag = someflag
    # end
    set = proc do |object, attribute|
      # if the package's attribute is currently nil *or* the flag setting for this
      # attribute is non-default, use the value.
      if object.send(attribute).nil? || send(attribute) != send("default_#{attribute}")
        @logger.info("Setting from flags: #{attribute}=#{send(attribute)}")
        object.send("#{attribute}=", send(attribute))
      end
    end
    set.call(input, :architecture)
    set.call(input, :category)
    set.call(input, :description)
    set.call(input, :epoch)
    set.call(input, :iteration)
    set.call(input, :license)
    set.call(input, :maintainer)
    set.call(input, :name)
    set.call(input, :url)
    set.call(input, :vendor)
    set.call(input, :version)
    set.call(input, :architecture)

    input.conflicts += conflicts
    input.dependencies += dependencies
    input.provides += provides
    input.replaces += replaces

    setscript = proc do |scriptname|
      path = self.send(scriptname)
      # Skip scripts not set
      next if path.nil?

      # 'self.send(scriptname) == self.before_install == --before-install
      if !File.exists?(path)
        $stderr.puts("No such file (for #{scriptname.to_s}): #{path.inspect}")
        return 1
      end
      input.scripts[scriptname] = File.read(path)
    end

    setscript.call(:before_install)
    setscript.call(:after_install)
    setscript.call(:before_remove)
    setscript.call(:after_remove)
    
    # Convert to the output type
    output = input.convert(output_class)

    # Write the output somewhere, package can be nil if no --package is specified, 
    # and that's OK.
    begin
      output.output(output.to_s(package))
    rescue FPM::Package::FileAlreadyExists => e
      @logger.fatal(e.message)
      return 1
    end

    return 0
  ensure
    input.cleanup unless input.nil?
    output.cleanup unless output.nil?
  end # def execute

  # A simple flag validator
  #
  # The goal of this class is to ensure the flags and arguments given
  # are a valid configuration.
  class Validator
    include FPM::Util
    private

    def initialize(command)
      @command = command
      @valid = true
      @messages = []

      validate
    end # def initialize

    def ok?
      return @valid
    end # def ok?

    def validate
      # Make sure the user has passed '-s' and '-t' flags
      mandatory(@command.input_type,
                "Missing required -s flag. What package source did you want?")
      mandatory(@command.output_type,
                "Missing required -t flag. What package output did you want?")

      # Verify the types requested are valid
      types = FPM::Package.types.keys.sort
      with(@command.input_type) do |val|
        next if val.nil?
        mandatory(FPM::Package.types.include?(val),
                  "Invalid input package -s flag) type #{val.inspect}. " \
                  "Expected one of: #{types.join(", ")}")
      end

      with(@command.output_type) do |val|
        next if val.nil?
        mandatory(FPM::Package.types.include?(val),
                  "Invalid output package (-t flag) type #{val.inspect}. " \
                  "Expected one of: #{types.join(", ")}")
      end

      mandatory(@command.args.any?,
                "No parameters given. You need to pass additional command " \
                "arguments so that I know what you want to build packages " \
                "from. For example, for '-s dir' you would pass a list of " \
                "files and directories. For '-s gem' you would pass a one" \
                " or more gems to package from. As a full example, this " \
                "will make an rpm of the 'json' rubygem: " \
                "`fpm -s gem -t rpm json`")
    end # def validate

    def mandatory(value, message)
      if value.nil? or !value
        @messages << message
        @valid = false
      end
    end # def mandatory

    def messages
      return @messages
    end # def messages

    public(:initialize, :ok?, :messages)
  end # class Validator
end # class FPM::Program