ptomulik/puppet-portsxutil

View on GitHub
lib/puppet/util/ptomulik/packagex/portsx/functions.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'puppet/util/ptomulik/packagex'

module Puppet::Util::PTomulik::Packagex::Portsx
# Stuff used commonly by other portsx-related modules.
#
# @note To gather certain information the module uses `ENV`, Facter and runs
#   external commands.
#
# The content of the module is described in the following subsections.
#
# #### Validating package names etc.
#
# To check whether a given value constitutes valid *portorigin*, *portname*,
# or *pkgname*, the following methods may be used:
#
# - {#portorigin?},
# - {#portname?},
# - {#pkgname?}.
#
# The module also defines constants containing regular expressions that may
# be used to validate *portorigins*, *pkgnames*, *portnames* etc.. These are:
#
# - {PORTNAME_RE},
# - {PKGNAME_RE},
# - {PORTORIGIN_RE},
# - {PORTVERSION_RE}.
#
# Note that these regexps are not mutually exclusive. Certain strings may
# match {PKGNAME_RE} and {PORTNAME_RE} simultaneously, for example.
#
# #### Preparing search patterns for `make search` command
#
# The {Puppet::Util::PTomulik::Packagex::Portsx::PortSearch} module defines
# several methods to search ports' INDEX. They accept (lists of) *portnames*,
# *pkgnames*, *portorigins*, etc., as search keys. The `make search` back-end
# command, however, needs to be provided with a search pattern that is a kind
# of regular expression. The search pattern may describe multiple ports at
# once. That said, the external command may query information for multiple
# packages at once. The search pattern must be thus properly constructed to
# generate expected search queries. The following methods are involved in
# pattern creation:
#
# - {#escape_pattern},
# - {#strings_to_pattern},
# - {#fullname_to_pattern},
# - {#pkgname_to_pattern},
# - {#portorigin_to_pattern},
# - {#portname_to_pattern},
# - {#mk_search_pattern},
#
# #### Determining location of port directories and type of database
#
# There are also methods returning (default) locations of ports tree and
# the root of ports database in a file system. These are:
#
# - {#portsdir},
# - {#port_dbdir}.
#
# There is also a method to check whether a local OS uses
# [pkgng](https://wiki.freebsd.org/pkgng), see:
#
# - {#pkgng_active?}.
#
module Functions

  # Regular expression used to match portname.
  PORTNAME_RE    = /[a-zA-Z0-9][\w\.+-]*/
  # Regular expression used to match package version suffix.
  PORTVERSION_RE = /[a-zA-Z0-9][\w\.,]*/
  # Regular expression used to match pkgname.
  PKGNAME_RE     = /(#{PORTNAME_RE})-(#{PORTVERSION_RE})/
  # Regular expression used to match portorigin.
  PORTORIGIN_RE  = /(#{PORTNAME_RE})\/(#{PORTNAME_RE})/

  # Is this a well-formed port's origin?
  #
  # @param s the value to be verified
  # @return [Boolean] `true` if `s` is a `String` and contains well-formed
  #   *portorigin*, `false` otherwise,
  #
  def portorigin?(s)
    s.is_a?(String) and s =~ /^#{PORTORIGIN_RE}$/
  end

  # Is this a well-formed port's pkgname?
  #
  # @param s the value to be verified
  # @return [Boolean] `true` if `s` is a `String` and contains well-formed
  #   *pkgname*, `false` othrewise
  #
  def pkgname?(s)
    s.is_a?(String) and s =~ /^#{PKGNAME_RE}$/
  end

  # Is this a well-formed portname?
  #
  # @param s the value to be verified
  # @return [Boolean] `true` if `s` is a `String` and contains well-formed
  #   *portname*, `false` otherwise
  #
  def portname?(s)
    s.is_a?(String) and s =~ /^#{PORTNAME_RE}$/
  end

  # Split *pkgname* into *portname* and *portversion*.
  #
  # @param pkgname [String] the name to be split
  # @return [Array] a 2-element array: `[portname,portversion]`; if the input
  #   string `pkgname` cannot be split into *portname* and *portversion*, the
  #   function still returns a 2-element array in form `[pkgname,nil]`
  #
  def split_pkgname(pkgname)
    if m = /^#{PKGNAME_RE}$/.match(pkgname)
      m.captures
    else
      [pkgname, nil]
    end
  end

  # Escape string that is to be used as a search pattern.
  #
  # This method converts all characters that could be interpreted as regex
  # special characters to corresponding escape sequences.
  #
  # @note The resultant pattern is a search pattern for an external CLI
  #   command and not a ruby regexp (it's a string in fact).
  # @param pattern [String] the search pattern to be escaped
  # @return [String] escaped pattern.
  #
  def escape_pattern(pattern)
    # it's also advisable to validate user's input with pkgname?, portname? or
    # potorigin?
    pattern.gsub(/([\(\)\.\*\[\]\|])/) {|c| '\\' + c}
  end

  # Convert a search key (or an array of keys) to a search pattern for `"make
  # search"` command.
  #
  # If `s` is a string, it's just escaped with {#escape_pattern} and returned.
  # If it's a sequence of strings, then the function returns a pattern matching
  # any of them.
  #
  # @note The resultant pattern is a search pattern for an external CLI
  #   command and not a ruby regexp (it's a string in fact).
  # @param s [String|Enumerable] a string or an array of strings to be
  #   converted to a search pattern
  # @return [String] the resultant search pattern 
  #
  def strings_to_pattern(s)
    if s.is_a?(Enumerable) and not s.instance_of?(String)
      '(' + s.map{|p| escape_pattern(p)}.join('|') + ')'
    else
      escape_pattern(s)
    end
  end

  # Convert a full package name to search pattern for the `make search` command.
  #
  # @param names [String|Enumerable] the name or names to be turned into
  #   search pattern,
  # @return [String] the resultant pattern
  # @see #strings_to_pattern
  #
  def fullname_to_pattern(names)
    "^#{strings_to_pattern(names)}$"
  end

  # Convert *portorigins* to search pattern for the `make search` command.
  #
  # @param origins [String|Enumerable] the *portorigin* or *portorigins* to be
  #   turned into search pattern,
  # @return [String] the resultant pattern
  # @see #strings_to_pattern
  #
  def portorigin_to_pattern(origins)
    "^#{portsdir}/#{strings_to_pattern(origins)}$"
  end

  # Convert *pkgnames* to search pattern for the `make search` command.
  #
  # @param pkgnames [String|Enumerable] the *pkgname* or *pkgnames* to be
  #   turned into search patterns,
  # @return [String] the resultant pattern
  # @see #strings_to_pattern
  #
  def pkgname_to_pattern(pkgnames)
    fullname_to_pattern(pkgnames)
  end

  # Convert *portnames* to search pattern for the `make search` command.
  #
  # @param portnames [String|Enumerable] the *portname* or *portnames* to be
  #   turned into search pattern,
  # @return [String] the resultant pattern
  # @see #strings_to_pattern
  #
  def portname_to_pattern(portnames)
    version_pattern = '[a-zA-Z0-9][a-zA-Z0-9\\.,_]*'
    "^#{strings_to_pattern(portnames)}-#{version_pattern}$"
  end

  # Convert *portorigins*, *pkgnames* or *portnames* to search pattern for the
  # `make search` command.
  #
  # What the function exactly does depends on `key`, that is for `:pkgname` it
  # does `pkgname_to_pattern(s)`, for `:portname` -> `portname_to_pattern(s)`
  # and so on.
  #
  # @param key [Symbol] decides how to process `s`, possible values are
  #   `:pkgname`, `:portname`, `:portorigin`. For other values, the function
  #   returns result of `fullname_to_pattern(s)`.
  # @param s [String|Enumerable] a string or a sequence of strings to be turned
  #   into search pattern,
  # @return [String] the resultant search pattern
  def mk_search_pattern(key, s)
    case key
    when :pkgname
      pkgname_to_pattern(s)
    when :portname
      portname_to_pattern(s)
    when :portorigin
      portorigin_to_pattern(s)
    else
      fullname_to_pattern(s)
    end
  end

  # Path to BSD ports source tree.
  # @note you may set `ENV['PORTSDIR']` to override defaults.
  # @return [String] `/usr/pkgsrc` on NetBSD, `/usr/ports` on other systems or
  #   the value defined by `ENV['PORTSDIR']`.
  #
  def portsdir
    unless dir = ENV['PORTSDIR']
      os = Facter.value(:operatingsystem)
      dir = (os == "NetBSD") ? '/usr/pkgsrc' : '/usr/ports'
    end
    dir
  end

  # Path to ports DB directory, defaults to `/var/db/ports`.
  # @note You may set `ENV['PORT_DBDIR']` to override defaults.
  # @return [String] `/var/db/ports`, or the value defined by `ENV['PORT_DBDIR'].
  #
  def port_dbdir
    unless dir = ENV['PORT_DBDIR']
      dir = '/var/db/ports'
    end
    dir
  end

  # Return standard names of option files for a port.
  #
  # When compiling a port, its Makefile may read build options from a set of
  # files. There is a convention to look for options in a predefined set of
  # options files whose names are derived from *portname* and *portorigin*. Up
  # to 4 option files may be read by Makefile (two for *portname* and two for
  # *portorigin*) and options contained in all the files get merged.
  #
  # This method returns for a given *portname* and *portorogin* a list of
  # files that may potentially contain build options for the port. The returned
  # names are in same order as they are read by ports Makefile's. The last file
  # overrides values defined in all previous file, so  it's most significant.
  # 
  # @param portname [String] the *portname* of a port,
  # @param portorigin [String] the *portorigin* for a port
  # @return [Array] an array of absolute paths to option files.
  #
  def options_files(portname, portorigin)
      [
        # keep these in proper order, see /usr/ports/Mk/bsd.options.mk
        portname,                  # OPTIONSFILE,
        portorigin.gsub(/\//,'_'), # OPTIONS_FILE,
      ].flatten.map{|x|
        f = File.join(self.port_dbdir,x,"options")
        [f,"#{f}.local"]
      }.flatten
  end

  # Check whether the pkgng is used by operating system.
  # 
  # This method uses technique proposed by `pkg(8)` man page to detect whether
  # the [pkgng](https://wiki.freebsd.org/pkgng) database is used by local OS.
  # The man page says:
  #
  #     The following script is the safest way to detect if pkg is installed
  #     and activated:
  #
  #         if TMPDIR=/dev/null ASSUME_ALWAYS_YES=1 \
  #             PACKAGESITE=file:///nonexistent \
  #             pkg info -x 'pkg(-devel)?$' >/dev/null 2>&1; then
  #           # pkgng-specifics
  #         else
  #           # pkg_install-specifics
  #         fi
  #
  # The method basically does the same but the commands are invoked from within
  # ruby.
  #
  # @param options [Hash] options to customize method behavior
  # @option options [String] :pkg path to the
  #   [pkg](http://www.freebsd.org/doc/handbook/pkgng-intro.html) command
  # @option options [String] :execpipe handle to a method which executes
  #   external commands in same way as puppet's `execpipe` does, , if not given,
  #   the Puppet::Util::Execution#execpipe is used. 
  # @return [Boolean] `true` if the pkgng is active or `false` otherwise.
  #
  def pkgng_active?(options = {})
    return @pkgng_active unless @pkgng_active.nil?

    pkg = options[:pkg] || (respond_to?(:command) ? command(:pkg) : nil)
    # Detect whether the OS uses old pkg or the new pkgng.
    @pkgng_active = false
    if pkg and FileTest.file?(pkg) and FileTest.executable?(pkg)
      ::Puppet.debug "'#{pkg}' command found, checking whether pkgng is active"
      env = { 'TMPDIR' => '/dev/null', 'ASSUME_ALWAYS_YES' => '1',
              'PACKAGESITE' => 'file:///nonexistent' }
      Puppet::Util.withenv(env) do 
        begin
          # this is technique proposed by pkg(8) man page,
          cmd = [pkg,'info','-x',"'pkg(-devel)?$'",'>/dev/null', '2>&1']
          execpipe = options[:execpipe] || Puppet::Util::Execution.method(:execpipe)
          execpipe.call(cmd) { |pipe| pipe.each_line {} } # just ignore
          @pkgng_active = true
        rescue Puppet::ExecutionFailure
        # nothing
        end
      end
    else
      ::Puppet.debug "'pkg' command not found"
    end
    ::Puppet.debug "pkgng is #{@pkgng_active ? '' : 'in'}active on this system"
    @pkgng_active
  end
end
end