#
# Copyright 2014-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

module Omnibus
  class Packager::Makeself < Packager::Base
    # @return [Hash]
    SCRIPT_MAP = {
      # Default Omnibus naming
      postinst: "postinst",
    }.freeze

    id :makeself

    setup do
      # Copy the full-stack installer into our scratch directory, accounting for
      # any excluded files.
      #
      # /opt/hamlet => /tmp/daj29013
      FileSyncer.sync(project.install_dir, staging_dir, exclude: exclusions)
    end

    build do
      # Write the scripts
      write_scripts

      # Write the makeselfinst file
      write_makeselfinst

      # Create the makeself archive
      create_makeself_package
    end

    # @see Base#package_name
    def package_name
      "#{project.package_name}-#{project.build_version}_#{project.build_iteration}.#{safe_architecture}.sh"
    end

    #
    # The path to the makeself script - the default should almost always be
    # fine!
    #
    # @return [String]
    #
    def makeself
      resource_path("makeself.sh")
    end

    #
    # The path to the makeself-header script - the default should almost always
    # be fine!
    #
    # @return [String]
    #
    def makeself_header
      resource_path("makeself-header.sh")
    end

    #
    # Render a makeselfinst in the staging directory using the supplied ERB
    # template. This file will be used to move the contents of the self-
    # extracting archive into place following extraction.
    #
    # @return [void]
    #
    def write_makeselfinst
      makeselfinst_staging_path = File.join(staging_dir, "makeselfinst")
      render_template(resource_path("makeselfinst.erb"),
        destination: makeselfinst_staging_path,
        variables: {
          install_dir: project.install_dir,
        }
      )
      FileUtils.chmod(0755, makeselfinst_staging_path)
    end

    #
    # Copy all scripts in {Project#package_scripts_path} to the staging
    # directory.
    #
    # @return [void]
    #
    def write_scripts
      SCRIPT_MAP.each do |source, destination|
        source_path = File.join(project.package_scripts_path, source.to_s)

        if File.file?(source_path)
          destination_path = File.join(staging_dir, destination)
          log.debug(log_key) { "Adding script `#{source}' to `#{destination_path}'" }
          copy_file(source_path, destination_path)
        end
      end
    end

    #
    # Run the actual makeself command, publishing the generated package.
    #
    # @return [void]
    #
    def create_makeself_package
      log.info(log_key) { "Creating makeself package" }

      Dir.chdir(staging_dir) do
        shellout! <<-EOH.gsub(/^ {10}/, "")
          #{makeself} \\
            --header "#{makeself_header}" \\
            --gzip \\
            "#{staging_dir}" \\
            "#{package_name}" \\
            "#{project.description}" \\
            "./makeselfinst"
        EOH
      end

      FileSyncer.glob("#{staging_dir}/*.sh").each do |makeself|
        copy_file(makeself, Config.package_dir)
      end
    end

    #
    # The architecture for this makeself package.
    #
    # @return [String]
    #
    def safe_architecture
      Ohai["kernel"]["machine"]
    end
  end
end


#
# Copyright 2014-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "digest"
require "fileutils"

module Omnibus
  class GitCache
    include Util
    include Logging

    # The serial number represents compatibility of a cache entry with the
    # current version of the omnibus code base. Any time a change is made to
    # omnibus that makes the code incompatible with any cache entries created
    # before the code change, the serial number should be incremented.
    #
    # For example, if a code change generates content in the `install_dir`
    # before cache snapshots are taken, any snapshots created before upgrade
    # will not have the generated content, so these snapshots would be
    # incompatible with the current omnibus codebase. Incrementing the serial
    # number ensures these old shapshots will not be used in subsequent builds.
    SERIAL_NUMBER = 3

    REQUIRED_GIT_FILES = %w{
HEAD
description
hooks
info
objects
refs}.freeze

    #
    # @return [Software]
    #
    attr_reader :software

    #
    # @param [Software] software
    #   the software this git cache is for
    #
    def initialize(software)
      @software = software
    end

    #
    # The path to the full install_dir cache for the project.
    #
    # @return [String]
    #
    def cache_path
      @cache_path ||= File.join(Config.git_cache_dir, install_dir)
    end

    #
    # Creates the full path if it does not exist already.
    #
    # @return [true, false]
    #   true if the path was created, false otherwise
    #
    def create_cache_path
      if File.directory?(cache_path)
        false
      else
        create_directory(File.dirname(cache_path))
        git_cmd("init -q")
        # On windows, git is very picky about single vs double quotes
        git_cmd("config --local user.name \"Omnibus Git Cache\"")
        git_cmd("config --local user.email \"omnibus@localhost\"")
        true
      end
    end

    #
    # Computes the tag for this cache entry.
    #
    # @return [String]
    #
    def tag
      return @tag if @tag

      log.internal(log_key) { "Calculating tag" }

      # Accumulate an array of all the software projects that come before
      # the name and version we are tagging. So if you have
      #
      # build_order = [ 1, 2, 3, 4, 5 ]
      #
      # And we are tagging 3, you would get dep_list = [ 1, 2 ]
      dep_list = software.project.library.build_order.take_while do |dep|
        if dep.name == software.name && dep.version == software.version
          false
        else
          true
        end
      end

      log.internal(log_key) { "dep_list: #{dep_list.map(&:name).inspect}" }

      # This is the list of all the unqiue shasums of all the software build
      # dependencies, including the on currently being acted upon.
      shasums = [dep_list.map(&:shasum), software.shasum].flatten
      suffix  = Digest::SHA256.hexdigest(shasums.join("|"))
      @tag    = "#{software.name}-#{suffix}-#{SERIAL_NUMBER}"

      log.internal(log_key) { "tag: #{@tag}" }

      @tag
    end

    # Create an incremental install path cache for the software step
    def incremental
      log.internal(log_key) { "Performing incremental cache" }

      create_cache_path
      remove_git_dirs

      git_cmd("add -A -f")

      begin
        git_cmd(%Q{commit -q -m "Backup of #{tag}"})
      rescue CommandFailed => e
        raise unless e.message.include?("nothing to commit")
      end

      git_cmd(%Q{tag -f "#{tag}"})
    end

    def restore
      log.internal(log_key) { "Performing cache restoration" }

      create_cache_path

      if has_tag(tag)
        log.internal(log_key) { "Detected tag `#{tag}' can be restored, marking it for restoration" }
        git_cmd(%Q{tag -f restore_here "#{tag}"})
        true
      elsif has_tag("restore_here")
        log.internal(log_key) { "Could not find tag `#{tag}', restoring previous tag" }
        restore_from_cache
        false
      else
        log.internal(log_key) { "Could not find marker tag `restore_here', nothing to restore" }
        false
      end
    end

    def restore_from_cache
      git_cmd("checkout -f restore_here")
    ensure
      git_cmd("tag -d restore_here")
    end

    #
    # Git caching will attempt to version embedded git directories, partially
    # versioning them. This causes failures on subsequent runs. This method
    # will find git directories and remove them to prevent those errors.
    #
    # @return [true]
    def remove_git_dirs
      log.internal(log_key) { "Removing git directories" }

      Dir.glob("#{install_dir}/**/{,.*}/config").reject do |path|
        REQUIRED_GIT_FILES.any? do |required_file|
          !File.exist?(File.join(File.dirname(path), required_file))
        end
      end.each do |path|
        log.internal(log_key) { "Removing git dir `#{path}'" }
        FileUtils.rm_rf(File.dirname(path))
      end

      true
    end

    private

    #
    # Shell out and invoke a git command in the context of the git cache.
    #
    # We explicitly disable autocrlf because we want bit-for-bit storage and
    # recovery of build output. Hashes calculated on output files will be
    # invalid if we muck around with files after they have been produced.
    #
    # @return [Mixlib::Shellout] the underlying command object.
    #
    def git_cmd(command)
      shellout!([
        "git",
        "-c core.autocrlf=false",
        "-c core.ignorecase=false",
        "--git-dir=\"#{cache_path}\"",
        "--work-tree=\"#{install_dir}\"",
        command,
      ].join(" "))
    end

    #
    #
    # The installation directory for this software's project. Drive letters are
    # stripped for Windows.
    #
    # @return [String]
    #
    def install_dir
      @install_dir ||= software.project.install_dir.sub(/^([A-Za-z]:)/, "")
    end

    # Override the log_key for this class to include the software name
    #
    # @return [String]
    def log_key
      @log_key ||= "#{super}: #{software.name}"
    end

    def has_tag(tag)
      cmd = git_cmd(%Q{tag -l "#{tag}"})
      cmd.stdout.lines.any? { |line| tag == line.chomp }
    end
  end
end


#
# Copyright 2016-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "omnibus/packagers/windows_base"

module Omnibus
  class Packager::APPX < Packager::WindowsBase
    id :appx

    setup do
      # Render the manifest
      write_manifest_file

      # Copy all the staging assets from vendored Omnibus into the resources
      # directory.
      FileSyncer.glob("#{Omnibus.source_root}/resources/#{id}/assets/*").each do |file|
        copy_file(file, "#{project.install_dir}/#{File.basename(file)}")
      end

      # Copy all assets in the user's project directory - this may overwrite
      # files copied in the previous step, but that's okay :)
      FileSyncer.glob("#{resources_path}/assets/*").each do |file|
        copy_file(file, "#{project.install_dir}/#{File.basename(file)}")
      end
    end

    build do
      # Pack the files with makeappx.exe, recursively generate fragment for
      # project directory
      Dir.chdir(staging_dir) do
        appx_file = windows_safe_path(Config.package_dir, package_name)
        shellout!(pack_command(appx_file))

        if signing_identity
          sign_package(appx_file)
        end
      end
    end

    # @see Base#package_name
    def package_name
      "#{project.package_name}-#{project.build_version}-#{project.build_iteration}-#{Config.windows_arch}.appx"
    end

    #
    # Write the manifest file into the staging directory.
    #
    # @return [void]
    #
    def write_manifest_file
      render_template(resource_path("AppxManifest.xml.erb"),
        destination: "#{windows_safe_path(project.install_dir)}/AppxManifest.xml",
        variables: {
          name: project.package_name,
          friendly_name: project.friendly_name,
          version: windows_package_version,
          maintainer: project.maintainer,
          certificate_subject: certificate_subject.gsub('"', """),
        }
      )
    end

    #
    # Get the shell command to run pack in order to create a
    # an appx package
    #
    # @return [String]
    #
    def pack_command(appx_file)
      "makeappx.exe pack /d \"#{windows_safe_path(project.install_dir)}\" /o /p #{appx_file}"
    end
  end
end


#
# Copyright 2013-2014 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "mixlib/shellout"

module Omnibus
  module Util
    def self.included(base)
      # This module requires logging is also available
      base.send(:include, Logging)
    end

    #
    # The default shellout options.
    #
    # @return [Hash]
    #
    SHELLOUT_OPTIONS = {
      log_level: :internal,
      timeout: 7200, # 2 hours
      environment: {},
    }.freeze

    #
    # The proper platform-specific "$PATH" key.
    #
    # @return [String]
    #
    def path_key
      # The ruby devkit needs ENV['Path'] set instead of ENV['PATH'] because
      # $WINDOWSRAGE, and if you don't set that your native gem compiles
      # will fail because the magic fixup it does to add the mingw compiler
      # stuff won't work.
      #
      # Turns out there is other build environments that only set ENV['PATH'] and if we
      # modify ENV['Path'] then it ignores that.  So, we scan ENV and returns the first
      # one that we find.
      #
      if windows?
        result = ENV.keys.grep(/\Apath\Z/i)
        case result.length
        when 0
          raise "The current omnibus environment has no PATH"
        when 1
          result.first
        else
          raise "The current omnibus environment has multiple PATH/Path variables."
        end
      else
        "PATH"
      end
    end

    #
    # Shells out and runs +command+.
    #
    # @overload shellout(command, options = {})
    #   @param command [String]
    #   @param options [Hash] the options passed to the initializer of the
    #     +Mixlib::ShellOut+ instance.
    # @overload shellout(command_fragments, options = {})
    #   @param command [Array<String>] command argv as individual strings
    #   @param options [Hash] the options passed to the initializer of the
    #     +Mixlib::ShellOut+ instance.
    # @return [Mixlib::ShellOut] the underlying +Mixlib::ShellOut+ instance
    #   which has +stdout+, +stderr+, +status+, and +exitstatus+
    #   populated with results of the command.
    #
    def shellout(*args)
      options = args.last.kind_of?(Hash) ? args.pop : {}
      options = SHELLOUT_OPTIONS.merge(options)

      command_string = args.join(" ")
      in_msys = options.delete(:in_msys_bash) && ENV["MSYSTEM"]
      # Mixlib will handle escaping characters for cmd but our command might
      # contain '. For now, assume that won't happen because I don't know
      # whether this command is going to be played via cmd or through
      # ProcessCreate.
      command_string = "bash -c \'#{command_string}\'" if in_msys

      # Grab the log_level
      log_level = options.delete(:log_level)

      # Set the live stream if one was not given
      options[:live_stream] ||= log.live_stream(:internal)

      # Since Mixlib::ShellOut supports :environment and :env, we want to
      # standardize here
      if options[:env]
        options[:environment] = options.fetch(:environment, {}).merge(options[:env])
      end

      # Log any environment options given
      unless options[:environment].empty?
        log.public_send(log_level, log_key) { "Environment:" }
        options[:environment].sort.each do |key, value|
          log.public_send(log_level, log_key) { "  #{key}=#{value.inspect}" }
        end
      end

      # Log the actual command
      log.public_send(log_level, log_key) { "$ #{command_string}" }

      cmd = Mixlib::ShellOut.new(command_string, options)
      cmd.environment["HOME"] = "/tmp" unless ENV["HOME"]
      cmd.run_command
      cmd
    end

    #
    # Similar to +shellout+ method except it raises an exception if the
    # command fails.
    #
    # @see #shellout
    #
    # @raise [CommandFailed]
    #   if +exitstatus+ is not in the list of +valid_exit_codes+
    # @raise [CommandTimeout]
    #   if execution time exceeds +timeout+
    #
    def shellout!(*args)
      cmd = shellout(*args)
      cmd.error!
      cmd
    rescue Mixlib::ShellOut::ShellCommandFailed
      raise CommandFailed.new(cmd)
    rescue Mixlib::ShellOut::CommandTimeout
      raise CommandTimeout.new(cmd)
    end

    #
    # Retry the given block if a retriable exception is
    # raised. Returns the value of the block call if successful.
    #
    # @param [String] logstr
    #   Description of the action being retried. Used in log output.
    #
    # @param [Array<Exception>] retried_exceptions
    #   List of exceptions to retry.  Any other exceptions are raisesd.
    #
    # @param [Integer] retries
    #   Number of times to retry the given block.
    #
    def retry_block(logstr, retried_exceptions = [], retries = Omnibus::Config.fetcher_retries, &block)
      yield
    rescue Exception => e
      raise e unless retried_exceptions.any? { |eclass| e.is_a?(eclass) }
      if retries != 0
        log.info(log_key) { "Retrying failed #{logstr} due to #{e} (#{retries} retries left)..." }
        retries -= 1
        retry
      else
        log.error(log_key) { "#{logstr} failed - #{e.class}!" }
        raise
      end
    end

    #
    # Convert the given path to be appropiate for shelling out on Windows.
    #
    # @param [String, Array<String>] pieces
    #   the pieces of the path to join and fix
    # @return [String]
    #   the path with applied changes
    #
    def windows_safe_path(*pieces)
      path = File.join(*pieces)

      if File::ALT_SEPARATOR
        path.gsub(File::SEPARATOR, File::ALT_SEPARATOR)
      else
        path
      end
    end

    #
    # Convert the given path to be appropriate for usage with the given compiler
    #
    # @param [String, Array<String>] pieces
    #   the pieces of the path to join and fix
    # @return [String]
    #   the path with applied changes
    #
    def compiler_safe_path(*pieces)
      path = File.join(*pieces)
      path = path.sub(/^([A-Za-z]):\//, "/\\1/") if ENV["MSYSTEM"]
      path
    end

    #
    # Create a directory at the given +path+.
    #
    # @param [String, Array<String>] paths
    #   the path or list of paths to join to create
    #
    # @return [String]
    #   the path to the created directory
    #
    def create_directory(*paths)
      path = File.join(*paths)
      log.debug(log_key) { "Creating directory `#{path}'" }
      FileUtils.mkdir_p(path)
      path
    end

    #
    # Remove the directory at the given +path+.
    #
    # @param [String, Array<String>] paths
    #   the path or list of paths to join to delete
    #
    # @return [String]
    #   the path to the removed directory
    #
    def remove_directory(*paths)
      path = File.join(*paths)
      log.debug(log_key) { "Remove directory `#{path}'" }
      FileUtils.rm_rf(path)
      path
    end

    #
    # Copy the +source+ file to the +destination+.
    #
    # @param [String] source
    # @param [String] destination
    #
    # @return [String]
    #   the destination path
    #
    def copy_file(source, destination)
      log.debug(log_key) { "Copying `#{source}' to `#{destination}'" }
      FileUtils.cp(source, destination)
      destination
    end

    #
    # Remove the file at the given path.
    #
    # @param [String, Array<String>] paths
    #   the path or list of paths to join to delete
    #
    # @return [String]
    #   the path to the removed file
    #
    def remove_file(*paths)
      path = File.join(*paths)
      log.debug(log_key) { "Removing file `#{path}'" }
      FileUtils.rm_f(path)
      path
    end

    #
    # Create a file at the given path. If a block is given, the contents of the
    # block are written to the file. If the block is not given, the file is
    # simply "touched".
    #
    # @param [String, Array<String>] paths
    #   the path or list of paths to join to create
    #
    # @return [String]
    #   the path to the created file
    #
    def create_file(*paths, &block)
      path = File.join(*paths)
      log.debug(log_key) { "Creating file `#{path}'" }

      FileUtils.mkdir_p(File.dirname(path))

      if block
        File.open(path, "wb") { |f| f.write(yield) }
      else
        FileUtils.touch(path)
      end

      path
    end

    #
    # Create a symlink from a to b
    #
    # @param [String] a
    # @param [String] b
    #
    def create_link(a, b)
      log.debug(log_key) { "Linking `#{a}' to `#{b}'" }
      FileUtils.ln_s(a, b)
    end
  end
end


#
# Copyright 2015-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "uri"
require "fileutils"
require "omnibus/download_helpers"
require "license_scout/collector"
require "license_scout/reporter"
require "license_scout/options"

module Omnibus
  class Licensing
    include Logging
    include DownloadHelpers
    include Sugarable

    OUTPUT_DIRECTORY = "LICENSES".freeze
    CACHE_DIRECTORY = "license-cache".freeze

    class << self

      # Creates a new instance of Licensing, executes preparation steps, then
      # yields control to a given block, and then creates a summary of the
      # included licenses.
      #
      # @example Building a project:
      #
      #   Licensing.create_incrementally(self) do |license_collector|
      #     softwares.each do |software|
      #       software.build_me([license_collector])
      #     end
      #   end
      #
      # @param [Project] project
      #   The project being built.
      #
      # @yieldparam [Licensing] license_collector
      #   Yields an instance of Licensing. Call #execute_post_build to copy the
      #   license files for a Software definition.
      #
      # @return [Licensing]
      #
      def create_incrementally(project)
        new(project).tap do |license_collector|

          license_collector.prepare
          license_collector.validate_license_info

          yield license_collector

          license_collector.process_transitive_dependency_licensing_info
          license_collector.create_project_license_file
          license_collector.raise_if_warnings_fatal!
        end
      end
    end

    #
    # The project to create licenses for.
    #
    # @return [Project]
    #
    attr_reader :project

    #
    # The warnings encountered while preparing the licensing information
    #
    # @return [Array<String>]
    #
    attr_reader :licensing_warnings

    #
    # The warnings encountered while preparing the licensing information for
    # transitive dependencies.
    #
    # @return [Array<String>]
    #
    attr_reader :transitive_dependency_licensing_warnings

    #
    # Manifest data of transitive dependency licensing information
    #
    # @return Hash
    #
    attr_reader :dep_license_map

    #
    # @param [Project] project
    #   the project to create licenses for.
    #
    def initialize(project)
      @project = project
      @licensing_warnings = []
      @transitive_dependency_licensing_warnings = []
      @dep_license_map = {}
    end

    #
    # Creates the required directories for licenses.
    #
    # @return [void]
    #
    def prepare
      FileUtils.rm_rf(output_dir)
      FileUtils.mkdir_p(output_dir)
      FileUtils.touch(output_dir_gitkeep_file)
      FileUtils.rm_rf(cache_dir)
      FileUtils.mkdir_p(cache_dir)
      FileUtils.touch(cache_dir_gitkeep_file)
    end

    # Required callback to use instances of this class as a build wrapper for
    # Software#build_me. Licensing doesn't need to do anything pre-build, so
    # this does nothing.
    #
    # @param [Software] software
    #
    # @return [void]
    #
    def execute_pre_build(software)
    end

    # Callback that gets called by Software#build_me after the build is done.
    # Invokes license copying for the given software. This ensures that
    # licenses are copied before a git cache snapshot is taken, so that the
    # license files are correctly restored when a build is skipped due to a
    # cache hit.
    #
    # @param [Software] software
    #
    # @return [void]
    #
    def execute_post_build(software)
      collect_licenses_for(software)
      unless software.skip_transitive_dependency_licensing
        collect_transitive_dependency_licenses_for(software)
        check_transitive_dependency_licensing_errors_for(software)
      end
    end

    #
    # Inspects the licensing information for the project and the included
    # software components. Logs the found issues to the log as warning.
    #
    # @return [void]
    #
    def validate_license_info
      # First check the project licensing information

      # Check existence of licensing information
      if project.license == "Unspecified"
        licensing_warning("Project '#{project.name}' does not contain licensing information.")
      end

      # Check license file exists
      if project.license != "Unspecified" && project.license_file.nil?
        licensing_warning("Project '#{project.name}' does not point to a license file.")
      end

      # Check used license is a standard license
      if project.license != "Unspecified" && !STANDARD_LICENSES.include?(project.license)
        licensing_info("Project '#{project.name}' is using '#{project.license}' which is not one of the standard licenses identified in https://opensource.org/licenses/alphabetical. Consider using one of the standard licenses.")
      end

      # Now let's check the licensing info for software components
      license_map.each do |software_name, license_info|
        # First check if the software specified a license
        if license_info[:license] == "Unspecified"
          licensing_warning("Software '#{software_name}' does not contain licensing information.")
        end

        # Check if the software specifies any license files
        if license_info[:license] != "Unspecified" && license_info[:license_files].empty?
          licensing_warning("Software '#{software_name}' does not point to any license files.")
        end

        # Check if the software license is one of the standard licenses
        if license_info[:license] != "Unspecified" && !STANDARD_LICENSES.include?(license_info[:license])
          licensing_info("Software '#{software_name}' uses license '#{license_info[:license]}' which is not one of the standard licenses identified in https://opensource.org/licenses/alphabetical. Consider using one of the standard licenses.")
        end
      end
    end

    #
    # Creates the top level license file for the project.
    # Top level file is created at #{project.license_file_path}
    # and contains the name of the project, version of the project,
    # text of the license of the project and a summary of the licenses
    # of the included software components.
    #
    # @return [void]
    #
    def create_project_license_file
      File.open(project.license_file_path, "w") do |f|
        f.puts "#{project.name} #{project.build_version} license: \"#{project.license}\""
        f.puts ""
        f.puts project_license_content
        f.puts ""
        f.puts components_license_summary
        f.puts ""
        f.puts dependencies_license_summary
      end
    end

    #
    # Contents of the project's license
    #
    # @return [String]
    #
    def project_license_content
      project.license_file.nil? ? "" : IO.read(File.join(Config.project_root, project.license_file))
    end

    #
    # Summary of the licenses included by the softwares of the project.
    # It is in the form of:
    # ...
    # This product bundles python 2.7.9,
    # which is available under a "Python" License.
    # For details, see:
    # /opt/opscode/LICENSES/python-LICENSE
    # ...
    #
    # @return [String]
    #
    def components_license_summary
      out = "\n\n"

      license_map.keys.sort.each do |name|
        license = license_map[name][:license]
        license_files = license_map[name][:license_files]
        version = license_map[name][:version]

        out << "This product bundles #{name} #{version},\n"
        out << "which is available under a \"#{license}\" License.\n"
        if !license_files.empty?
          out << "For details, see:\n"
          license_files.each do |license_file|
            out << "#{license_package_location(name, license_file)}\n"
          end
        end
        out << "\n"
      end

      out
    end

    #
    # Summary of the licenses of the transitive dependencies of the project.
    # It is in the form of:
    # ...
    # This product includes inifile 3.0.0
    # which is a 'ruby_bundler' dependency of 'chef',
    # and which is available under a 'MIT' License.
    # For details, see:
    # /opt/opscode/LICENSES/ruby_bundler-inifile-3.0.0-README.md
    # ...
    #
    # @return [String]
    #
    def dependencies_license_summary
      out = "\n\n"

      dep_license_map.each do |dep_mgr_name, data|
        data.each do |dep_name, data|
          data.each do |dep_version, dep_data|
            projects = dep_data["dependency_of"].sort.map { |p| "'#{p}'" }.join(", ")
            files = dep_data["license_files"].map { |f| File.join(output_dir, f) }

            out << "This product includes #{dep_name} #{dep_version}\n"
            out << "which is a '#{dep_mgr_name}' dependency of #{projects},\n"
            out << "and which is available under a '#{dep_data["license"]}' License.\n"
            out << "For details, see:\n"
            out << files.join("\n")
            out << "\n\n"
          end
        end
      end

      out
    end

    #
    # Map that collects information about the licenses of the softwares
    # included in the project.
    #
    # @example
    # {
    #   ...
    #   "python" => {
    #     "license" => "Python",
    #     "license_files" => "LICENSE",
    #     "version" => "2.7.9",
    #     "project_dir" => "/var/cache/omnibus/src/python/Python-2.7.9/"
    #   },
    #   ...
    # }
    #
    # @return [Hash]
    #
    def license_map
      @license_map ||= begin
        map = {}

        project.library.each do |component|
          # Some of the components do not bundle any software but contain
          # some logic that we use during the build. These components are
          # covered under the project's license and they do not need specific
          # license files.
          next if component.license == :project_license

          map[component.name] = {
            license: component.license,
            license_files: component.license_files,
            version: component.version,
            project_dir: component.project_dir,
          }
        end

        map
      end
    end

    #
    # Returns the location where the license file should reside in the package.
    # License file is named as <project_name>-<license_file_name> and created
    # under the output licenses directory.
    #
    # @return [String]
    #
    def license_package_location(component_name, where)
      if local?(where)
        File.join(output_dir, "#{component_name}-#{File.split(where).last}")
      else
        u = URI(where)
        File.join(output_dir, "#{component_name}-#{File.basename(u.path)}")
      end
    end

    #
    # Output directory to create the licenses in.
    #
    # @return [String]
    #
    def output_dir
      File.expand_path(OUTPUT_DIRECTORY, project.install_dir)
    end

    #
    # Path to a .gitkeep file we create in the output dir so git caching
    # doesn't delete the directory.
    #
    # @return [String]
    #
    def output_dir_gitkeep_file
      File.join(output_dir, ".gitkeep")
    end

    # Cache directory where transitive dependency licenses will be collected in.
    #
    # @return [String]
    #
    def cache_dir
      File.expand_path(CACHE_DIRECTORY, project.install_dir)
    end

    #
    # Path to a .gitkeep file we create in the cache dir so git caching
    # doesn't delete the directory.
    #
    # @return [String]
    #
    def cache_dir_gitkeep_file
      File.join(cache_dir, ".gitkeep")
    end

    #
    # Returns if the given path to a license is local or a remote url.
    #
    # @return [Boolean]
    #
    def local?(license)
      u = URI(license)
      u.scheme.nil?
    end

    #
    # Logs the given message as info.
    #
    # This method should only be used for detecting in a license is known or not.
    # In the future, we will introduce a configurable way to whitelist or blacklist
    # the allowed licenses. Once we implement that we need to stop using this method.
    #
    # @param [String] message
    #   message to log as warning
    def licensing_info(message)
      log.info(log_key) { message }
    end

    #
    # Logs the given message as warning or fails the build depending on the
    # :fatal_licensing_warnings configuration setting.
    #
    # @param [String] message
    #   message to log as warning
    def licensing_warning(message)
      licensing_warnings << message
      log.warn(log_key) { message }
    end

    #
    # Logs the given message as warning or fails the build depending on the
    # :fatal_transitive_dependency_licensing_warnings configuration setting.
    #
    # @param [String] message
    #   message to log as warning
    def transitive_dependency_licensing_warning(message)
      transitive_dependency_licensing_warnings << message
      log.warn(log_key) { message }
    end

    def raise_if_warnings_fatal!
      warnings_to_raise = []
      if Config.fatal_licensing_warnings && !licensing_warnings.empty?
        warnings_to_raise << licensing_warnings
      end

      if Config.fatal_transitive_dependency_licensing_warnings && !transitive_dependency_licensing_warnings.empty?
        warnings_to_raise << transitive_dependency_licensing_warnings
        warnings_to_raise << "If you are encountering missing license or missing license file errors for **transitive** dependencies, you can provide overrides for the missing information at https://github.com/chef/license_scout/blob/master/lib/license_scout/overrides.rb#L93"
      end

      warnings_to_raise.flatten!
      raise LicensingError.new(warnings_to_raise) unless warnings_to_raise.empty?
    end

    # 0. Translate all transitive dependency licensing issues into omnibus warnings
    # 1. Parse all the licensing information for all software from 'cache_dir'
    # 2. Merge and drop the duplicates
    # 3. Add these licenses to the main manifest, to be merged with the main
    # licensing information from software definitions.
    def process_transitive_dependency_licensing_info
      Dir.glob("#{cache_dir}/*/*-dependency-licenses.json").each do |license_manifest_path|
        license_manifest_data = FFI_Yajl::Parser.parse(File.read(license_manifest_path))
        project_name = license_manifest_data["project_name"]
        dependency_license_dir = File.dirname(license_manifest_path)

        license_manifest_data["dependency_managers"].each do |dep_mgr_name, dependencies|
          dep_license_map[dep_mgr_name] ||= {}

          dependencies.each do |dependency|
            # Copy dependency files
            dependency["license_files"].each do |f|
              license_path = File.join(dependency_license_dir, f)
              output_path = File.join(output_dir, f)
              FileUtils.cp(license_path, output_path)
            end

            dep_name = dependency["name"]
            dep_version = dependency["version"]

            # If we already have this dependency we do not need to add it again.
            if dep_license_map[dep_mgr_name][dep_name] && dep_license_map[dep_mgr_name][dep_name][dep_version]
              dep_license_map[dep_mgr_name][dep_name][dep_version]["dependency_of"] << project_name
            else
              dep_license_map[dep_mgr_name][dep_name] ||= {}
              dep_license_map[dep_mgr_name][dep_name][dep_version] = {
                "license" => dependency["license"],
                "license_files" => dependency["license_files"],
                "dependency_of" => [ project_name ],
              }
            end
          end
        end
      end

      FileUtils.rm_rf(cache_dir)
    end

    private

    # Uses license_scout to collect the licenses for transitive dependencies
    # into #{output_dir}/license-cache/#{software.name}
    def collect_transitive_dependency_licenses_for(software)
      # We collect the licenses of the transitive dependencies of this software
      # with LicenseScout. We place these files under
      # /opt/project-name/license-cache for them to be cached in git_cache. Once
      # the build completes we will process these license files but we need to
      # perform this step after build, before git_cache to be able to operate
      # correctly with the git_cache.

      collector = LicenseScout::Collector.new(
        software.name,
        software.project_dir,
        license_output_dir(software),
        LicenseScout::Options.new(
          environment: software.with_embedded_path,
          ruby_bin: software.embedded_bin("ruby"),
          manual_licenses: software.dependency_licenses
        )
      )

      begin
        # We do not automatically collect dependency licensing information when
        # skip_transitive_dependency_licensing is set on the software.
        collector.run
      rescue LicenseScout::Exceptions::UnsupportedProjectType => e
        # Looks like this project is not supported by LicenseScout. Either the
        # language and the dependency manager used by the project is not
        # supported, or the software definition does not have any transitive
        # dependencies.  In the latter case software definition should set
        # 'skip_transitive_dependency_licensing' to 'true' to correct this
        # error.
        transitive_dependency_licensing_warning(<<-EOH)
Software '#{software.name}' is not supported project type for transitive \
dependency license collection. See https://github.com/chef/license_scout for \
the list of supported languages and dependency managers. If this project does \
not have any transitive dependencies, consider setting \
'skip_transitive_dependency_licensing' to 'true' in order to correct this error.
EOH
        # If we got here, we need to fail now so we don't take a git
        # cache snapshot, or else the software build could be restored
        # from cache without fixing the license issue.
        raise_if_warnings_fatal!
      rescue LicenseScout::Exceptions::Error => e
        transitive_dependency_licensing_warning(<<-EOH)
Can not automatically detect licensing information for '#{software.name}' using \
license_scout. Error is: '#{e}'
EOH
        # If we got here, we need to fail now so we don't take a git
        # cache snapshot, or else the software build could be restored
        # from cache without fixing the license issue.
        raise_if_warnings_fatal!
      rescue Exception => e
        # This catch all exception handling is here in order not to fail builds
        # until license_scout gets more stable. As we are adding support for more
        # and more dependency managers we discover unhandled edge cases which
        # requires us to have this. Remove this once license_scout is stable.
        transitive_dependency_licensing_warning(<<-EOH)
Unexpected error while running license_scout for '#{software.name}': '#{e}'
EOH
        # If we got here, we need to fail now so we don't take a git
        # cache snapshot, or else the software build could be restored
        # from cache without fixing the license issue.
        raise_if_warnings_fatal!
      end
    end

    # Checks transitive dependency licensing errors for the given software
    def check_transitive_dependency_licensing_errors_for(software)
      reporter = LicenseScout::Reporter.new(license_output_dir(software))
      begin
        reporter.report.each { |i| transitive_dependency_licensing_warning(i) }
      rescue LicenseScout::Exceptions::InvalidOutputReport => e
        transitive_dependency_licensing_warning(<<-EOH)
Licensing output report at '#{license_output_dir(software)}' has errors:
#{e}
EOH
      end
      raise_if_warnings_fatal!
    end

    # The directory to store the licensing information for the given software
    def license_output_dir(software)
      File.join(cache_dir, software.name)
    end

    # Collect the license files for the software.
    def collect_licenses_for(software)
      return nil if software.license == :project_license

      software_name = software.name
      license_data = license_map[software_name]
      license_files = license_data[:license_files]

      license_files.each do |license_file|
        if license_file
          output_file = license_package_location(software_name, license_file)

          if local?(license_file)
            input_file = File.expand_path(license_file, license_data[:project_dir])
            if File.exist?(input_file)
              FileUtils.cp(input_file, output_file)
              File.chmod 0644, output_file unless windows?
            else
              licensing_warning("License file '#{input_file}' does not exist for software '#{software_name}'.")
              # If we got here, we need to fail now so we don't take a git
              # cache snapshot, or else the software build could be restored
              # from cache without fixing the license issue.
              raise_if_warnings_fatal!
            end
          else
            begin
              download_file!(license_file, output_file, enable_progress_bar: false)
              File.chmod 0644, output_file unless windows?
            rescue SocketError,
                   Errno::ECONNREFUSED,
                   Errno::ECONNRESET,
                   Errno::ENETUNREACH,
                   Timeout::Error,
                   OpenURI::HTTPError,
                   OpenSSL::SSL::SSLError
              licensing_warning("Can not download license file '#{license_file}' for software '#{software_name}'.")
              # If we got here, we need to fail now so we don't take a git
              # cache snapshot, or else the software build could be restored
              # from cache without fixing the license issue.
              raise_if_warnings_fatal!
            end
          end
        end
      end
    end

    STANDARD_LICENSES = [
      #
      # Below licenses are compiled based on https://opensource.org/licenses/alphabetical
      #
      "AFL-3.0",       # Academic Free License 3.0
      "AGPL-3.0",      # Affero General Public License
      "APL-1.0",       # Adaptive Public License
      "Apache-2.0",    # Apache License 2.0
      "APSL-2.0",      # Apple Public Source License
      "Artistic-1.0",  # Artistic license 1.0
      "Artistic-2.0",  # Artistic license 2.0
      "AAL",           # Attribution Assurance Licenses
      "BSD-3-Clause",  # BSD 3-Clause "New" or "Revised" License
      "BSD-2-Clause",  # BSD 2-Clause "Simplified" or "FreeBSD" License
      "BSL-1.0",       # Boost Software License
      "CECILL-2.1",    # CeCILL License 2.1
      "CATOSL-1.1",    # Computer Associates Trusted Open Source License 1.1
      "CDDL-1.0",      # Common Development and Distribution License 1.0
      "CPAL-1.0",      # Common Public Attribution License 1.0
      "CUA-OPL-1.0",   # CUA Office Public License Version 1.0
      "EUDatagrid",    # EU DataGrid Software License
      "EPL-1.0",       # Eclipse Public License 1.0
      "eCos-2.0",      # eCos License version 2.0
      "ECL-2.0",       # Educational Community License, Version 2.0
      "EFL-2.0",       # Eiffel Forum License V2.0
      "Entessa",       # Entessa Public License
      "EUPL-1.1",      # European Union Public License, Version 1.1
      "Fair",          # Fair License
      "Frameworx-1.0", # Frameworx License
      "FPL-1.0.0",     # Free Public License 1.0.0
      "AGPL-3.0",      # GNU Affero General Public License v3
      "GPL-2.0",       # GNU General Public License version 2.0
      "GPL-3.0",       # GNU General Public License version 3.0
      "LGPL-2.0",      # GNU Library or "Lesser" General Public License version 2.0
      "LGPL-2.1",      # GNU Library or "Lesser" General Public License version 2.1
      "LGPL-3.0",      # GNU Library or "Lesser" General Public License version 3.0
      "HPND",          # Historical Permission Notice and Disclaimer
      "IPL-1.0",       # IBM Public License 1.0
      "IPA",           # IPA Font License
      "ISC",           # ISC License
      "LPPL-1.3c",     # LaTeX Project Public License 1.3c
      "LiLiQ-P",       # Licence Libre du Quebec Permissive
      "LiLiQ-R",       # Licence Libre du Quebec Reciprocite
      "LiLiQ-R+",      # Licence Libre du Quebec Reciprocite forte
      "LPL-1.02",      # Lucent Public License Version 1.02
      "MirOS",         # MirOS Licence
      "MS-PL",         # Microsoft Public License
      "MS-RL",         # Microsoft Reciprocal License
      "MIT",           # MIT license
      "Motosoto",      # Motosoto License
      "MPL-2.0",       # Mozilla Public License 2.0
      "Multics",       # Multics License
      "NASA-1.3",      # NASA Open Source Agreement 1.3
      "NTP",           # NTP License
      "Naumen",        # Naumen Public License
      "NGPL",          # Nethack General Public License
      "Nokia",         # Nokia Open Source License
      "NPOSL-3.0",     # Non-Profit Open Software License 3.0
      "OCLC-2.0",      # OCLC Research Public License 2.0
      "OGTSL",         # Open Group Test Suite License
      "OSL-3.0",       # Open Software License 3.0
      "OPL-2.1",       # OSET Public License version 2.1
      "PHP-3.0",       # PHP License 3.0
      "PostgreSQL",    # The PostgreSQL License
      "Python-2.0",    # Python License
      "CNRI-Python",   # CNRI Python license
      "QPL-1.0",       # Q Public License
      "RPSL-1.0",      # RealNetworks Public Source License V1.0
      "RPL-1.5",       # Reciprocal Public License 1.5
      "RSCPL",         # Ricoh Source Code Public License
      "OFL-1.1",       # SIL Open Font License 1.1
      "SimPL-2.0",     # Simple Public License 2.0
      "Sleepycat",     # Sleepycat License
      "SPL-1.0",       # Sun Public License 1.0
      "Watcom-1.0",    # Sybase Open Watcom Public License 1.0
      "NCSA",          # University of Illinois/NCSA Open Source License
      "UPL",           # Universal Permissive License
      "VSL-1.0",       # Vovida Software License v. 1.0
      "W3C",           # W3C License
      "WXwindows",     # wxWindows Library License
      "Xnet",          # X.Net License
      "0BSD",          # Zero Clause BSD License
      "ZPL-2.0",       # Zope Public License 2.0
      "Zlib",          # zlib/libpng license
      #
      # In addition to these we would like to add some of the licenses that
      # are frequently used in our depedencies.
      #
      "Public-Domain", # https://opensource.org/faq#public-domain
      "Ruby",          # http://www.ruby-lang.org/en/LICENSE.txt
      "Erlang-Public", # http://www.erlang.org/EPLICENSE
      "Oracle-Binary", # http://www.oracle.com/technetwork/java/javase/terms/license/index.html
      "OpenSSL",       # https://www.openssl.org/source/license.html
      "Chef-MLSA",     # https://www.chef.io/online-master-agreement/
      "Perl-5",        # http://dev.perl.org/licenses/
    ].freeze
  end

end


#
# Copyright 2014-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "socket"

module Omnibus
  class Packager::Solaris < Packager::Base
    # @return [Hash]
    SCRIPT_MAP = {
      # Default Omnibus naming
      postinst: "postinstall",
      postrm: "postremove",
      # Default Solaris naming
      postinstall: "postinstall",
      postremove: "postremove",
    }.freeze

    id :solaris

    build do
      write_scripts
      write_prototype_file
      write_pkginfo_file
      create_solaris_file
    end

    # @see Base#package_name
    def package_name
      "#{project.package_name}-#{pkgmk_version}.#{safe_architecture}.solaris"
    end

    def pkgmk_version
      "#{project.build_version}-#{project.build_iteration}"
    end

    def install_dirname
      File.dirname(project.install_dir)
    end

    def install_basename
      File.basename(project.install_dir)
    end

    #
    # Copy all scripts in {Project#package_scripts_path} to the control
    # directory of this repo.
    #
    # @return [void]
    #
    def write_scripts
      SCRIPT_MAP.each do |source, destination|
        source_path = File.join(project.package_scripts_path, source.to_s)

        next unless File.file?(source_path)

        destination_path = staging_dir_path(destination)
        log.debug(log_key) { "Adding script `#{source}' to `#{destination_path}'" }
        copy_file(source_path, destination_path)
      end
    end

    #
    # Generate a Prototype file for solaris build
    #
    def write_prototype_file
      shellout! "cd #{install_dirname} && find #{install_basename} -print > #{staging_dir_path('files')}"

      File.open staging_dir_path("files.clean"), "w+" do |fout|
        File.open staging_dir_path("files") do |fin|
          fin.each_line do |line|
            if line.chomp =~ /\s/
              log.warn(log_key) { "Skipping packaging '#{line}' file due to whitespace in filename" }
            else
              fout.write(line)
            end
          end
        end
      end

      # generate list of control files
      File.open staging_dir_path("Prototype"), "w+" do |f|
        f.write <<-EOF.gsub(/^ {10}/, "")
          i pkginfo
          i postinstall
          i postremove
        EOF
      end

      # generate the prototype's file list
      shellout! "cd #{install_dirname} && pkgproto < #{staging_dir_path('files.clean')} > #{staging_dir_path('Prototype.files')}"

      # fix up the user and group in the file list to root
      shellout! "awk '{ $5 = \"root\"; $6 = \"root\"; print }' < #{staging_dir_path('Prototype.files')} >> #{staging_dir_path('Prototype')}"
    end

    #
    # Generate a pkginfo file for solaris build
    #
    def write_pkginfo_file
      hostname = Socket.gethostname

      # http://docs.oracle.com/cd/E19683-01/816-0219/6m6njqbat/index.html
      pkginfo_content = <<-EOF.gsub(/^ {8}/, "")
        CLASSES=none
        TZ=PST
        PATH=/sbin:/usr/sbin:/usr/bin:/usr/sadm/install/bin
        BASEDIR=#{install_dirname}
        PKG=#{project.package_name}
        NAME=#{project.package_name}
        ARCH=#{safe_architecture}
        VERSION=#{pkgmk_version}
        CATEGORY=application
        DESC=#{project.description}
        VENDOR=#{project.maintainer}
        EMAIL=#{project.maintainer}
        PSTAMP=#{hostname}#{Time.now.utc.iso8601}
      EOF
      File.open staging_dir_path("pkginfo"), "w+" do |f|
        f.write pkginfo_content
      end
    end

    #
    # Generate the Solaris file using +pkg*+.
    #
    # @return [void]
    #
    def create_solaris_file
      shellout! "pkgmk -o -r #{install_dirname} -d #{staging_dir} -f #{staging_dir_path('Prototype')}"
      shellout! "pkgchk -vd #{staging_dir} #{project.package_name}"
      shellout! "pkgtrans #{staging_dir} #{package_path} #{project.package_name}"
    end

    #
    # The architecture for this Solaris package.
    #
    # @return [String]
    #
    def safe_architecture
      # The #i386? and #intel? helpers come from chef-sugar
      if intel?
        "i386"
      elsif sparc?
        "sparc"
      else
        Ohai["kernel"]["machine"]
      end
    end
  end
end


#
# Copyright 2014-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "ffi_yajl"

module Omnibus
  class Package
    include Digestable

    #
    # @return [String]
    #
    attr_reader :path

    #
    # Create a new package from the given path.
    #
    # @param [String] path
    #   the path to the package on disk
    #
    def initialize(path)
      @path = File.expand_path(path)
    end

    #
    # The shortname of this package (the basename of the file).
    #
    # @return [String]
    #
    def name
      @name ||= File.basename(path)
    end

    #
    # The MD5 checksum for this file.
    #
    # @return [String]
    #
    def md5
      @md5 ||= digest(path, :md5)
    end

    #
    # The SHA1 checksum for this file.
    #
    # @return [String]
    #
    def sha1
      @sha1 ||= digest(path, :sha1)
    end

    #
    # The SHA256 checksum for this file.
    #
    # @return [String]
    #
    def sha256
      @sha256 ||= digest(path, :sha256)
    end

    #
    # The SHA512 checksum for this file.
    #
    # @return [String]
    #
    def sha512
      @sha512 ||= digest(path, :sha512)
    end

    #
    # The actual contents of the package.
    #
    # @return [String]
    #
    def content
      @content ||= IO.read(path)
    rescue Errno::ENOENT
      raise NoPackageFile.new(path)
    end

    #
    # The parsed contents of the metadata.
    #
    # @raise [NoPackageMetadataFile] if the {#metadata} does not exist
    # @raise [FFI_Yajl::ParseError] if the JSON is not valid
    #
    # @return [Hash<Symbol, String>]
    #
    def metadata
      @metadata ||= Metadata.for_package(self)
    end

    #
    # Set the metadata for this package
    #
    # @param [Metadata] metadata
    #
    def metadata=(metadata)
      @metadata = metadata
    end

    #
    # Validate the presence of the required components for the package.
    #
    # @raise [NoPackageFile] if the package is not present
    # @raise [NoPackageMetadataFile] if the metadata file is not present
    #
    # @return [true]
    #
    def validate!
      unless File.exist?(path)
        raise NoPackageFile.new(path)
      end

      unless File.exist?(metadata.path)
        raise NoPackageMetadataFile.new(metadata.path)
      end

      true
    end
  end
end


#
# Copyright 2012-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "omnibus/s3_helpers"

module Omnibus
  class S3Publisher < Publisher
    include S3Helpers

    def publish(&block)
      log.info(log_key) { "Starting S3 publisher" }

      packages.each do |package|
        # Make sure the package is good to go!
        log.debug(log_key) { "Validating '#{package.name}'" }
        package.validate!

        # Upload the metadata first
        log.debug(log_key) { "Uploading '#{package.metadata.name}'" }

        s3_metadata_object = store_object(
          key_for(package, package.metadata.name),
          FFI_Yajl::Encoder.encode(package.metadata.to_hash, pretty: true),
          nil,
          access_policy
        )

        log.debug(log_key) { "Uploading is completed. Download URL (#{access_policy}): #{s3_metadata_object.public_url}" }

        # Upload the actual package
        log.info(log_key) { "Uploading '#{package.name}'" }

        s3_object = store_object(
          key_for(package, package.name),
          package.content,
          package.metadata[:md5],
          access_policy
        )

        log.info(log_key) { "Uploading is completed. Download URL (#{access_policy}): #{s3_object.public_url}" }

        # If a block was given, "yield" the package to the caller
        yield(package) if block
      end
    end

    private

    def s3_configuration
      config = {
        region: @options[:region],
        bucket_name: @options[:bucket],
      }

      if Config.publish_s3_profile
        config[:profile]            = Config.publish_s3_profile
      else
        config[:access_key_id]      = Config.publish_s3_access_key
        config[:secret_access_key]  = Config.publish_s3_secret_key
      end

      config
    end

    #
    # The unique upload key for this package. The additional "stuff" is
    # postfixed to the end of the path.
    #
    # @example
    #   'el/6/x86_64/chef-11.6.0-1.el6.x86_64.rpm/chef-11.6.0-1.el6.x86_64.rpm'
    #
    # @param [Package] package
    #   the package this key is for
    # @param [Array<String>] stuff
    #   the additional things to append
    #
    # @return [String]
    #
    def key_for(package, *stuff)
      File.join(
        Config.s3_publish_pattern % package.metadata,
        *stuff
      )
    end

    #
    # The access policy that corresponds to the +s3_access+ given in the
    # initializer option. Any access control that is not the strict string
    # +"public"+ is assumed to be private.
    #
    # @return [String]
    #   the access policy
    #
    def access_policy
      if @options[:acl].to_s == "public"
        "public-read"
      else
        "private"
      end
    end
  end
end


#
# Copyright 2012-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "time"

module Omnibus
  # Provides methods for generating Omnibus project build version
  # strings automatically from Git repository information.
  #
  # @see Omnibus::Project#build_version
  #
  # @note Requires a Git repository
  #
  # @todo Rename this class to reflect its absolute dependence on running in a
  #   Git repository.
  class BuildVersion
    include Logging
    include Util

    # Formatting string for the timestamp component of our SemVer build specifier.
    #
    # @see Omnibus::BuildVersion#semver
    # @see Time#strftime
    TIMESTAMP_FORMAT = "%Y%m%d%H%M%S".freeze

    class << self
      # @see (BuildVersion#git_describe)
      def git_describe
        new.git_describe
      end

      # @see (BuildVersion#semver)
      def semver
        new.semver
      end

      def build_start_time
        new.build_start_time
      end
    end

    # Create a new BuildVersion
    #
    # @param [String] path
    #   Path from which to read git version information
    def initialize(path = Config.project_root)
      @path = path
    end

    # @!group Version Generator Methods

    # Generate a {http://semver.org/ SemVer 2.0.0-rc.1 compliant}
    # version string for an Omnibus project.
    #
    # This relies on the Omnibus project being a Git repository, as
    # well as having tags named according to SemVer conventions
    # (specifically, the `MAJOR.MINOR.PATCH-PRERELEASE` aspects)
    #
    # The specific format of the version string is:
    #
    #     MAJOR.MINOR.PATCH-PRERELEASE+TIMESTAMP.git.COMMITS_SINCE.GIT_SHA
    #
    # By default, a timestamp is incorporated into the build component of
    # version string (see {Omnibus::BuildVersion::TIMESTAMP_FORMAT}). This
    # option is configurable via the {Config}.
    #
    # @example 11.0.0-alpha.1+20121218164140.git.207.694b062
    # @return [String]
    # @see #git_describe
    # @todo Issue a warning or throw an exception if the tags of the
    #   repository are not themselves SemVer-compliant?
    # @todo Consider making the {#build_start_time} method public, as
    #   its function influences how build timestamps are generated,
    #   and can be influenced by users.
    def semver
      build_tag = version_tag

      # PRERELEASE VERSION
      if prerelease_version?
        # ensure all dashes are dots per precedence rules (#12) in Semver
        # 2.0.0-rc.1
        prerelease = prerelease_tag.tr("-", ".")
        build_tag << "-" << prerelease
      end

      # BUILD VERSION
      # Follows SemVer conventions and the build version begins with a '+'.
      build_version_items = []

      # By default we will append a timestamp to every build. This behavior can
      # be overriden by setting the OMNIBUS_APPEND_TIMESTAMP environment
      # variable to a 'falsey' value (ie false, f, no, n or 0).
      #
      # format: YYYYMMDDHHMMSS example: 20130131123345
      if Config.append_timestamp
        build_version_items << build_start_time
      end

      # We'll append the git describe information unless we are sitting right
      # on an annotated tag.
      #
      # format: git.COMMITS_SINCE_TAG.GIT_SHA example: git.207.694b062
      unless commits_since_tag == 0
        build_version_items << ["git", commits_since_tag, git_sha_tag].join(".")
      end

      unless build_version_items.empty?
        build_tag << "+" << build_version_items.join(".")
      end

      build_tag
    end

    # We'll attempt to retrieve the timestamp from the Jenkin's set BUILD_TIMESTAMP
    # or fall back to BUILD_ID environment variable. This will ensure platform specfic
    # packages for the same build will share the same timestamp.
    def build_start_time
      @build_start_time ||= begin
                              if ENV["BUILD_TIMESTAMP"]
                                begin
                                  Time.strptime(ENV["BUILD_TIMESTAMP"], "%Y-%m-%d_%H-%M-%S")
                                rescue ArgumentError
                                  error_message =  "BUILD_TIMESTAMP environment variable "
                                  error_message << "should be in YYYY-MM-DD_hh-mm-ss "
                                  error_message << "format."
                                  raise ArgumentError, error_message
                                end
                              elsif ENV["BUILD_ID"]
                                begin
                                  Time.strptime(ENV["BUILD_ID"], "%Y-%m-%d_%H-%M-%S")
                                rescue ArgumentError
                                  error_message =  "BUILD_ID environment variable "
                                  error_message << "should be in YYYY-MM-DD_hh-mm-ss "
                                  error_message << "format."
                                  raise ArgumentError, error_message
                                end
                              else
                                Time.now.utc
                              end
                            end.strftime(TIMESTAMP_FORMAT)
    end

    # Generates a version string by running
    # {https://www.kernel.org/pub/software/scm/git/docs/git-describe.html
    # git describe} in the root of the Omnibus project.
    #
    # Produces a version string of the format
    #
    #     MOST_RECENT_TAG-COMMITS_SINCE-gGIT_SHA
    #
    # @example
    #  11.0.0-alpha.1-207-g694b062
    # @return [String]
    def git_describe
      @git_describe ||= begin
        cmd = shellout("git describe --tags", cwd: @path)

        if cmd.exitstatus == 0
          cmd.stdout.chomp
        else
          log.warn(log_key) do
            "Could not extract version information from 'git describe'! " \
            "Setting version to 0.0.0."
          end
          "0.0.0"
        end
      end
    end

    # @!endgroup

    # Note: The remaining methods could just as well be private

    # Return a `MAJOR.MINOR.PATCH` version string, as extracted from
    # {#git_describe}.
    #
    # Here are some illustrative examples:
    #
    #     1.2.7-208-ge908a52 -> 1.2.7
    #     11.0.0-alpha-59-gf55b180 -> 11.0.0
    #     11.0.0-alpha2 -> 11.0.0
    #     10.16.2.rc.1 -> 10.16.2
    #
    # @return [String]
    def version_tag
      version_composition.join(".")
    end

    # Return a prerelease tag string (if it exists), as extracted from {#git_describe}.
    #
    # Here are some illustrative examples:
    #
    #     1.2.7-208-ge908a52 -> nil
    #     11.0.0-alpha-59-gf55b180 -> alpha
    #     11.0.0-alpha2 -> alpha2
    #     10.16.2.rc.1 -> rc.1
    #
    # @return [String] if a pre-release tag was found
    # @return [nil] if no pre-release tag was found
    def prerelease_tag
      prerelease_regex = if commits_since_tag > 0
                           /^v?\d+\.\d+\.\d+(?:-|\.)([0-9A-Za-z.-]+)-\d+-g[0-9a-f]+$/
                         else
                           /^v?\d+\.\d+\.\d+(?:-|\.)([0-9A-Za-z.-]+)$/
                         end
      match = prerelease_regex.match(git_describe)
      match ? match[1] : nil
    end

    # Extracts the 7-character truncated Git SHA1 from the output of {#git_describe}.
    #
    # Here are some illustrative examples:
    #
    #     1.2.7-208-ge908a52 -> e908a52
    #     11.0.0-alpha-59-gf55b180 -> f55b180
    #     11.0.0-alpha2 -> nil
    #     10.16.2.rc.1 -> nil
    #
    # @return [String] the truncated SHA1
    # @return [nil] if no SHA1 is present in the output of #{git_describe}
    def git_sha_tag
      sha_regexp = /g([0-9a-f]+)$/
      match = sha_regexp.match(git_describe)
      match ? match[1] : nil
    end

    # Extracts the number of commits since the most recent Git tag, as
    # determined by {#git_describe}.
    #
    # Here are some illustrative examples:
    #
    #     1.2.7-208-ge908a52 -> 208
    #     11.0.0-alpha-59-gf55b180 -> 59
    #     11.0.0-alpha2 -> 0
    #     10.16.2.rc.1 -> 0
    #
    # @return [Fixnum]
    def commits_since_tag
      commits_regexp = /^.*-(\d+)\-g[0-9a-f]+$/
      match = commits_regexp.match(git_describe)
      match ? match[1].to_i : 0
    end

    # Indicates whether the version represents a pre-release or not, as
    # signalled by the presence of a pre-release tag in the version
    # string.
    #
    # @return [Boolean]
    # @see #prerelease_tag
    def prerelease_version?
      if prerelease_tag
        true
      else
        false
      end
    end

    private

    # Pulls out the major, minor, and patch components from the output
    # of {#git_describe}.
    #
    # Relies on the most recent Git tag being SemVer compliant (i.e.,
    # starting with a `MAJOR.MINOR.PATCH` string)
    #
    # @return [Array<(String, String, String)>]
    #
    # @todo Compute this once and store the result in an instance variable
    def version_composition
      version_regexp = /^v?(\d+)\.(\d+)\.(\d+)/

      if match = version_regexp.match(git_describe)
        match[1..3]
      else
        raise "Invalid semver tag `#{git_describe}'!"
      end
    end
  end
end


#
# Copyright 2014-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "ffi_yajl"

module Omnibus
  class Metadata
    extend Sugarable
    include Sugarable

    class << self
      #
      # Render the metadata for the package at the given path, generated by the
      # given project.
      #
      # @raise [NoPackageFile]
      #   if the given +path+ does not contain a package
      #
      # @param [String] path
      #   the path to the package (or compressed object) on disk
      # @param [Project] project
      #   the project which generated the given package or compressed object
      #
      # @return [String]
      #   the path to the metadata on disk
      #
      def generate(path, project)
        unless File.exist?(path)
          raise NoPackageFile.new(path)
        end

        package = Package.new(path)

        data = {
          # Package
          basename: package.name,
          md5: package.md5,
          sha1: package.sha1,
          sha256: package.sha256,
          sha512: package.sha512,
          platform: platform_shortname,
          platform_version: platform_version,
          arch: arch,

          # Project
          name: project.name,
          friendly_name: project.friendly_name,
          homepage: project.homepage,
          version: project.build_version,
          iteration: project.build_iteration,
          license: project.license,
          version_manifest: project.built_manifest.to_hash,
          license_content: File.exist?(project.license_file_path) ? File.read(project.license_file_path) : "",
        }

        instance = new(package, data)
        instance.save
        instance.path
      end

      #
      # Load the metadata from disk.
      #
      # @param [Package] package
      #   the package for this metadata
      #
      # @return [Metadata]
      #
      def for_package(package)
        data = File.read(path_for(package))
        hash = FFI_Yajl::Parser.parse(data, symbolize_names: true)

         # Ensure Platform version has been truncated
        if hash[:platform_version] && hash[:platform]
          hash[:platform_version] = truncate_platform_version(hash[:platform_version], hash[:platform])
        end

        # Ensure an interation exists
        hash[:iteration] ||= 1

        new(package, hash)
      rescue Errno::ENOENT
        raise NoPackageMetadataFile.new(package.path)
      end

      #
      # The metadata path that corresponds to the package.
      #
      # @param [Package] package
      #   the package for this metadata
      #
      # @return [String]
      #
      def path_for(package)
        "#{package.path}.metadata.json"
      end

      #
      # The architecture for this machine, as reported from Ohai.
      #
      # @return [String]
      #
      def arch
        if windows? && windows_arch_i386?
          "i386"
        elsif solaris?
          if intel?
            "i386"
          elsif sparc?
            "sparc"
          end
        else
          Ohai["kernel"]["machine"]
        end
      end

      #
      # Platform version to be used in package metadata.
      #
      # @return [String]
      #   the platform version
      #
      def platform_version
        truncate_platform_version(Ohai["platform_version"], platform_shortname)
      end

      #
      # Platform name to be used when creating metadata for the artifact.
      #
      # @return [String]
      #   the platform family short name
      #
      def platform_shortname
        if rhel?
          "el"
        elsif suse?
          "sles"
        else
          Ohai["platform"]
        end
      end

      private

      #
      # On certain platforms we don't care about the full MAJOR.MINOR.PATCH platform
      # version. This method will properly truncate the version down to a more human
      # friendly version. This version can also be thought of as a 'marketing'
      # version.
      #
      # @param [String] platform_version
      #   the platform version to truncate
      # @param [String] platform
      #   the platform shortname. this might be an Ohai-returned platform or
      #   platform family but it also might be a shortname like `el`
      #
      # rubocop:disable Lint/DuplicateCaseCondition
      def truncate_platform_version(platform_version, platform)
        case platform
        when "centos", "debian", "el", "fedora", "freebsd", "omnios", "pidora", "raspbian", "rhel", "sles", "suse", "smartos"
          # Only want MAJOR (e.g. Debian 7, OmniOS r151006, SmartOS 20120809T221258Z)
          platform_version.split(".").first
        when "aix", "alpine", "mac_os_x", "openbsd", "slackware", "solaris2", "opensuse", "opensuseleap", "ubuntu", "amazon"
          # Only want MAJOR.MINOR (e.g. Mac OS X 10.9, Ubuntu 12.04)
          platform_version.split(".")[0..1].join(".")
        when "arch", "gentoo"
          # Arch Linux / Gentoo do not have a platform_version ohai attribute, they are rolling release (lsb_release -r)
          "rolling"
        when "windows"
          # Windows has this really awesome "feature", where their version numbers
          # internally do not match the "marketing" name.
          #
          # Definitively computing the Windows marketing name actually takes more
          # than the platform version. Take a look at the following file for the
          # details:
          #
          #   https://github.com/chef/chef/blob/master/lib/chef/win32/version.rb
          #
          # As we don't need to be exact here the simple mapping below is based on:
          #
          #  http://www.jrsoftware.org/ishelp/index.php?topic=winvernotes
          #
          # Microsoft's version listing (more general than the above) is here:
          #
          # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724832(v=vs.85).aspx
          #
          case platform_version
          when "5.0.2195", "2000"   then "2000"
          when "5.1.2600", "xp"     then "xp"
          when "5.2.3790", "2003r2" then "2003r2"
          when "6.0.6001", "2008"   then "2008"
          when "6.1.7600", "7"      then "7"
          when "6.1.7601", "2008r2" then "2008r2"
          when "6.2.9200", "2012"   then "2012"
          # The following `when` will never match since Windows 8's platform
          # version is the same as Windows 2012. It's only here for completeness and
          # documentation.
          when "6.2.9200", "8"      then "8"
          when /6\.3\.\d+/, "2012r2" then "2012r2"
          # The following `when` will never match since Windows 8.1's platform
          # version is the same as Windows 2012R2. It's only here for completeness
          # and documentation.
          when /6\.3\.\d+/, "8.1" then "8.1"
          when /^10\.0/ then "10"
          else
            raise UnknownPlatformVersion.new(platform, platform_version)
          end
        else
          raise UnknownPlatform.new(platform)
        end
      end
    end

    #
    # Create a new metadata object for the given package and hash data.
    #
    # @param [Package] package
    #   the package for this metadata
    # @param [Hash] data
    #   the hash of attributes to set in the metadata
    #
    def initialize(package, data = {})
      @package = package
      @data    = data.dup.freeze
    end

    #
    # Helper for accessing the information inside the metadata hash.
    #
    # @return [Object]
    #
    def [](key)
      @data[key]
    end

    #
    # The name of this metadata file.
    #
    # @return [String]
    #
    def name
      @name ||= File.basename(path)
    end

    #
    # @see (Metadata.path_for)
    #
    def path
      @path ||= self.class.path_for(@package)
    end

    #
    # Save the file to disk.
    #
    # @return [true]
    #
    def save
      File.open(path, "w+") do |f|
        f.write(FFI_Yajl::Encoder.encode(to_hash, pretty: true))
      end

      true
    end

    #
    # Hash representation of this metadata.
    #
    # @return [Hash]
    #
    def to_hash
      @data.dup
    end

    #
    # The JSON representation of this metadata.
    #
    # @return [String]
    #
    def to_json
      FFI_Yajl::Encoder.encode(@data, pretty: true)
    end
  end
end


#
# Copyright 2014-2018 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# need to make sure rpmbuild is installed

module Omnibus
  class Packager::RPM < Packager::Base
    # @return [Hash]
    SCRIPT_MAP = {
      # Default Omnibus naming
      preinst: "pre",
      postinst: "post",
      prerm: "preun",
      postrm: "postun",
      # Default RPM naming
      pre: "pre",
      post: "post",
      preun: "preun",
      postun: "postun",
      verifyscript: "verifyscript",
      pretrans: "pretrans",
      posttrans: "posttrans",
    }.freeze

    id :rpm

    setup do
      # Create our magic directories
      create_directory("#{staging_dir}/BUILD")
      create_directory("#{staging_dir}/RPMS")
      create_directory("#{staging_dir}/SRPMS")
      create_directory("#{staging_dir}/SOURCES")
      create_directory("#{staging_dir}/SPECS")

      # Copy the full-stack installer into the SOURCE directory, accounting for
      # any excluded files.
      #
      # /opt/hamlet => /tmp/daj29013/BUILD/opt/hamlet
      destination = File.join(build_dir, project.install_dir)
      FileSyncer.sync(project.install_dir, destination, exclude: exclusions)

      # Copy over any user-specified extra package files.
      #
      # Files retain their relative paths inside the scratch directory, so
      # we need to grab the dirname of the file, create that directory, and
      # then copy the file into that directory.
      #
      # extra_package_file '/path/to/foo.txt' #=> /tmp/BUILD/path/to/foo.txt
      project.extra_package_files.each do |file|
        parent      = File.dirname(file)
        destination = File.join("#{staging_dir}/BUILD", parent)

        create_directory(destination)
        copy_file(file, destination)
      end
    end

    build do
      # Generate the spec
      write_rpm_spec

      # Generate the rpm
      create_rpm_file
    end

    #
    # @!group DSL methods
    # --------------------------------------------------

    #
    # Set or return the signing passphrase. If this value is provided,
    # Omnibus will attempt to sign the RPM.
    #
    # @example
    #   signing_passphrase "foo"
    #
    # @param [String] val
    #   the passphrase to use when signing the RPM
    #
    # @return [String]
    #   the RPM-signing passphrase
    #
    def signing_passphrase(val = NULL)
      if null?(val)
        @signing_passphrase
      else
        @signing_passphrase = val
      end
    end
    expose :signing_passphrase

    #
    # Set or return the vendor who made this package.
    #
    # @example
    #   vendor "Seth Vargo <sethvargo@gmail.com>"
    #
    # @param [String] val
    #   the vendor who make this package
    #
    # @return [String]
    #   the vendor who make this package
    #
    def vendor(val = NULL)
      if null?(val)
        @vendor || "Omnibus <omnibus@getchef.com>"
      else
        unless val.is_a?(String)
          raise InvalidValue.new(:vendor, "be a String")
        end

        @vendor = val
      end
    end
    expose :vendor

    #
    # Set or return the license for this package.
    #
    # @example
    #   license "Apache 2.0"
    #
    # @param [String] val
    #   the license for this package
    #
    # @return [String]
    #   the license for this package
    #
    def license(val = NULL)
      if null?(val)
        @license || project.license
      else
        unless val.is_a?(String)
          raise InvalidValue.new(:license, "be a String")
        end

        @license = val
      end
    end
    expose :license

    #
    # Set or return the priority for this package.
    #
    # @example
    #   priority "extra"
    #
    # @param [String] val
    #   the priority for this package
    #
    # @return [String]
    #   the priority for this package
    #
    def priority(val = NULL)
      if null?(val)
        @priority || "extra"
      else
        unless val.is_a?(String)
          raise InvalidValue.new(:priority, "be a String")
        end

        @priority = val
      end
    end
    expose :priority

    #
    # Set or return the category for this package.
    #
    # @example
    #   category "databases"
    #
    # @param [String] val
    #   the category for this package
    #
    # @return [String]
    #   the category for this package
    #
    def category(val = NULL)
      if null?(val)
        @category || "default"
      else
        unless val.is_a?(String)
          raise InvalidValue.new(:category, "be a String")
        end

        @category = val
      end
    end
    expose :category

    #
    # Set or return the dist_tag for this package
    #
    # The Dist Tag for this RPM package as per the Fedora packaging guidlines.
    #
    # @see http://fedoraproject.org/wiki/Packaging:DistTag
    #
    # @example
    #   dist_tag ".#{Omnibus::Metadata.platform_shortname}#{Omnibus::Metadata.platform_version}"
    #
    # @param [String] val
    #   the dist_tag for this package
    #
    # @return [String]
    #   the dist_tag for this package
    #
    def dist_tag(val = NULL)
      if null?(val)
        @dist_tag || ".#{Omnibus::Metadata.platform_shortname}#{Omnibus::Metadata.platform_version}"
      else
        @dist_tag = val
      end
    end
    expose :dist_tag

    #
    # Set or return the compression type (:gzip, :bzip2, :xz) for this package
    #
    # @example
    #   compression_type :xz
    #
    # @param [Symbol] val
    #   the compression type
    #
    # @return [String]
    #   the compression type for this package
    #
    def compression_type(val = NULL)
      if null?(val)
        @compression_type || :gzip
      else
        unless val.is_a?(Symbol) && [:gzip, :bzip2, :xz].member?(val)
          raise InvalidValue.new(:compression_type, "be a Symbol (:gzip, :bzip2, or :xz)")
        end

        @compression_type = val
      end
    end
    expose :compression_type

    #
    # Set or return the compression level (1-9) for this package
    #
    # @example
    #   compression_level 6
    #
    # @param [Integer] val
    #   the compression level
    #
    # @return [Integer]
    #   the compression level for this package
    #
    def compression_level(val = NULL)
      if null?(val)
        @compression_level || 9
      else
        unless val.is_a?(Integer) && 1 <= val && 9 >= val
          raise InvalidValue.new(:compression_level, "be an Integer (between 1 and 9)")
        end

        @compression_level = val
      end
    end
    expose :compression_level

    #
    # @!endgroup
    # --------------------------------------------------

    #
    # @return [String]
    #
    def package_name
      if dist_tag
        "#{safe_base_package_name}-#{safe_version}-#{safe_build_iteration}#{dist_tag}.#{safe_architecture}.rpm"
      else
        "#{safe_base_package_name}-#{safe_version}-#{safe_build_iteration}.#{safe_architecture}.rpm"
      end
    end

    #
    # The path to the +BUILD+ directory inside the staging directory.
    #
    # @return [String]
    #
    def build_dir
      @build_dir ||= File.join(staging_dir, "BUILD")
    end

    #
    # Get a list of user-declared config files
    #
    # @return [Array]
    #
    def config_files
      @config_files ||= project.config_files.map { |file| rpm_safe(file) }
    end

    #
    # Directories owned by the filesystem package:
    # http://fedoraproject.org/wiki/Packaging:Guidelines#File_and_Directory_Ownership
    #
    # @return [Array]
    #
    def filesystem_directories
      @filesystem_directories ||= IO.readlines(resource_path("filesystem_list")).map { |f| f.chomp }
    end

    #
    # Mark filesystem directories with ownership and permissions specified in the filesystem package
    # https://git.fedorahosted.org/cgit/filesystem.git/plain/filesystem.spec
    #
    # @return [String]
    #
    def mark_filesystem_directories(fsdir)
      if fsdir.eql?("/") || fsdir.eql?("/usr/lib") || fsdir.eql?("/usr/share/empty")
        "%dir %attr(0555,root,root) #{fsdir}"
      elsif filesystem_directories.include?(fsdir)
        "%dir %attr(0755,root,root) #{fsdir}"
      else
        "%dir #{fsdir}"
      end
    end

    #
    # Render an rpm spec file in +SPECS/#{name}.spec+ using the supplied ERB
    # template.
    #
    # @return [void]
    #
    def write_rpm_spec
      # Create a map of scripts that exist and their contents
      scripts = SCRIPT_MAP.inject({}) do |hash, (source, destination)|
        path =  File.join(project.package_scripts_path, source.to_s)

        if File.file?(path)
          hash[destination] = File.read(path)
        end

        hash
      end

      # Get a list of all files
      files = FileSyncer.glob("#{build_dir}/**/*")
                .map { |path| build_filepath(path) }

      render_template(resource_path("spec.erb"),
        destination: spec_file,
        variables: {
          name: safe_base_package_name,
          version: safe_version,
          iteration: safe_build_iteration,
          vendor: vendor,
          license: license,
          dist_tag: dist_tag,
          maintainer: project.maintainer,
          homepage: project.homepage,
          description: project.description,
          priority: priority,
          category: category,
          conflicts: project.conflicts,
          replaces: project.replaces,
          dependencies: project.runtime_dependencies,
          user: project.package_user,
          group: project.package_group,
          scripts: scripts,
          config_files: config_files,
          files: files,
          build_dir: build_dir,
          platform_family: Ohai["platform_family"],
          compression: compression,
        }
      )
    end

    #
    # Returns the RPM spec "_binary_payload" line corresponding to the
    # compression configuration.
    #
    # @return [String]
    #
    def compression
      compression_name = case compression_type
                         when :bzip2
                           "bzdio"
                         when :xz
                           "xzdio"
                         else # default to gzip
                           "gzdio"
                         end
      "w#{compression_level}.#{compression_name}"
    end

    #
    # Generate the RPM file using +rpmbuild+. Unlike debian,the +fakeroot+
    # command is not required for the package to be owned by +root:root+. The
    # rpmuser specified in the spec file dictates this.
    #
    # @return [void]
    #
    def create_rpm_file
      command =  %{rpmbuild}
      command << %{ --target #{safe_architecture}}
      command << %{ -bb}
      command << %{ --buildroot #{staging_dir}/BUILD}
      command << %{ --define '_topdir #{staging_dir}'}

      if signing_passphrase
        log.info(log_key) { "Signing enabled for .rpm file" }

        if File.exist?("#{ENV['HOME']}/.rpmmacros")
          log.info(log_key) { "Detected .rpmmacros file at `#{ENV['HOME']}'" }
          home = ENV["HOME"]
        else
          log.info(log_key) { "Using default .rpmmacros file from Omnibus" }

          # Generate a temporary home directory
          home = Dir.mktmpdir

          render_template(resource_path("rpmmacros.erb"),
            destination: "#{home}/.rpmmacros",
            variables: {
              gpg_name: project.maintainer,
              gpg_path: "#{ENV['HOME']}/.gnupg", # TODO: Make this configurable
            }
          )
        end

        command << " --sign"
        command << " #{spec_file}"

        with_rpm_signing do |signing_script|
          log.info(log_key) { "Creating .rpm file" }
          shellout!("#{signing_script} \"#{command}\"", environment: { "HOME" => home })
        end
      else
        log.info(log_key) { "Creating .rpm file" }
        command << " #{spec_file}"
        shellout!("#{command}")
      end

      FileSyncer.glob("#{staging_dir}/RPMS/**/*.rpm").each do |rpm|
        copy_file(rpm, Config.package_dir)
      end
    end

    #
    # Convert the path of a file in the staging directory to an entry for use in the spec file.
    #
    # @return [String]
    #
    def build_filepath(path)
      filepath = rpm_safe("/" + path.gsub("#{build_dir}/", ""))
      return if config_files.include?(filepath)
      full_path = build_dir + filepath.gsub("[%]", "%")
      # FileSyncer.glob quotes pathnames that contain spaces, which is a problem on el7
      full_path.delete!('"')
      # Mark directories with the %dir directive to prevent rpmbuild from counting their contents twice.
      return mark_filesystem_directories(filepath) if !File.symlink?(full_path) && File.directory?(full_path)
      filepath
    end

    #
    # The full path to this spec file on disk.
    #
    # @return [String]
    #
    def spec_file
      "#{staging_dir}/SPECS/#{package_name}.spec"
    end

    #
    # Render the rpm signing script with secure permissions, call the given
    # block with the path to the script, and ensure deletion of the script from
    # disk since it contains sensitive information.
    #
    # @param [Proc] block
    #   the block to call
    #
    # @return [String]
    #
    def with_rpm_signing(&block)
      directory   = Dir.mktmpdir
      destination = "#{directory}/sign-rpm"

      render_template(resource_path("signing.erb"),
        destination: destination,
        mode: 0700,
        variables: {
          passphrase: signing_passphrase,
        }
      )

      # Yield the destination to the block
      yield(destination)
    ensure
      remove_file(destination)
      remove_directory(directory)
    end

    #
    # Generate an RPM-safe name from the given string, doing the following:
    #
    # - Replace [ with [\[] to make rpm not use globs
    # - Replace * with [*] to make rpm not use globs
    # - Replace ? with [?] to make rpm not use globs
    # - Replace % with [%] to make rpm not expand macros
    #
    # @param [String] string
    #   the string to sanitize
    #
    def rpm_safe(string)
      string = "\"#{string}\"" if string[/\s/]

      string.dup
        .gsub("[", "[\\[]")
        .gsub("*", "[*]")
        .gsub("?", "[?]")
        .gsub("%", "[%]")
    end

    #
    # Return the RPM-ready base package name, converting any invalid characters to
    # dashes (+-+).
    #
    # @return [String]
    #
    def safe_base_package_name
      if project.package_name =~ /\A[a-z0-9\.\+\-]+\z/
        project.package_name.dup
      else
        converted = project.package_name.downcase.gsub(/[^a-z0-9\.\+\-]+/, "-")

        log.warn(log_key) do
          "The `name' component of RPM package names can only include " \
          "lowercase alphabetical characters (a-z), numbers (0-9), dots (.), " \
          "plus signs (+), and dashes (-). Converting `#{project.package_name}' to " \
          "`#{converted}'."
        end

        converted
      end
    end

    #
    # This is actually just the regular build_iternation, but it felt lonely
    # among all the other +safe_*+ methods.
    #
    # @return [String]
    #
    def safe_build_iteration
      project.build_iteration
    end

    #
    # RPM package versions cannot contain dashes, so we will convert them to
    # underscores.
    #
    # @return [String]
    #
    def safe_version
      version = project.build_version.dup

      # RPM 4.10+ added support for using the tilde (~) as a way to mark
      # versions as lower priority in comparisons. More details on this
      # feature can be found here:
      #
      #   http://rpm.org/ticket/56
      #
      if version =~ /\-/
        if Ohai["platform_family"] == "wrlinux"
          converted = version.tr("-", "_") # WRL has an elderly RPM version
          log.warn(log_key) do
            "Omnibus replaces dashes (-) with tildes (~) so pre-release " \
            "versions get sorted earlier than final versions.  However, the " \
            "version of rpmbuild on Wind River Linux does not support this. " \
            "All dashes will be replaced with underscores (_). Converting " \
            "`#{project.build_version}' to `#{converted}'."
          end
        else
          converted = version.tr("-", "~")
          log.warn(log_key) do
            "Tildes hold special significance in the RPM package versions. " \
            "They mark a version as lower priority in RPM's version compare " \
            "logic. We'll replace all dashes (-) with tildes (~) so pre-release" \
            "versions get sorted earlier then final versions. Converting" \
            "`#{project.build_version}' to `#{converted}'."
          end
        end

        version = converted
      end

      if version =~ /\A[a-zA-Z0-9\.\+\~]+\z/
        version
      else
        converted = version.gsub(/[^a-zA-Z0-9\.\+\~]+/, "_")

        log.warn(log_key) do
          "The `version' component of RPM package names can only include " \
          "alphabetical characters (a-z, A-Z), numbers (0-9), dots (.), " \
          "plus signs (+), tildes (~) and underscores (_). Converting " \
          "`#{project.build_version}' to `#{converted}'."
        end

        converted
      end
    end

    #
    # The architecture for this RPM package.
    #
    # @return [String]
    #
    def safe_architecture
      case Ohai["kernel"]["machine"]
      when "i686"
        "i386"
      when "armv6l"
        if Ohai["platform"] == "pidora"
          "armv6hl"
        else
          "armv6l"
        end
      else
        Ohai["kernel"]["machine"]
      end
    end
  end
end


#
# Copyright 2013-2014 Chef Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

module Omnibus
  class Command::Publish < Command::Base
    namespace :publish

    # This option is useful for publish packages that were built for a
    # particular platform/version but tested on other platform/versions.
    #
    # For example, one might build on Ubuntu 10.04 and test/publish on
    # Ubuntu 10.04, 12.04, and 14.04.
    #
    # @example JSON
    #   {
    #     "ubuntu-10.04": [
    #       "ubuntu-10.04",
    #       "ubuntu-12.04",
    #       "ubuntu-14.04"
    #     ]
    #   }
    #
    class_option :platform_mappings,
      desc: "The optional platform mappings JSON file to publish with",
      type: :string

    class_option :version_manifest,
      desc: "Path to the version-manifest.json file to publish with",
      type: :string

    #
    # Publish to S3.
    #
    #   $ omnibus publish s3 buckethands pkg/chef*
    #
    method_option :acl,
      type: :string,
      desc: "The accessibility of the uploaded packages",
      enum: %w{public private},
      default: "private"
    method_option :region,
      type: :string,
      desc: "The region in which the bucket is located",
      default: "us-east-1"
    desc "s3 BUCKET PATTERN", "Publish to an S3 bucket"
    def s3(bucket, pattern)
      options[:bucket] = bucket
      publish(S3Publisher, pattern, options)
    end

    #
    # Publish to artifactory.
    #
    #   $ omnibus publish artifactory libs-omnibus-local pkg/chef*
    #
    method_option :build_record,
      type: :boolean,
      desc: "Optionally create an Artifactory build record for the published artifacts",
      default: true
    method_option :properties,
      type: :hash,
      desc: "Properites to attach to published artifacts",
      default: {}
    desc "artifactory REPOSITORY PATTERN", "Publish to an Artifactory instance"
    def artifactory(repository, pattern)
      if options[:version_manifest]
        Omnibus.logger.deprecated("ArtifactoryPublisher") do
          "The `--version-manifest' option has been deprecated. Version manifest data is now part of the `*.metadata.json' file"
        end
      end

      options[:repository] = repository
      publish(ArtifactoryPublisher, pattern, options)
    end

    private

    #
    # Shortcut method for executing a publisher.
    #
    # @return [void]
    #
    def publish(klass, pattern, options)
      if options[:platform_mappings]
        options[:platform_mappings] = FFI_Yajl::Parser.parse(File.read(File.expand_path(options[:platform_mappings])))
      end

      klass.publish(pattern, options) do |package|
        say("Published '#{package.name}' for #{package.metadata[:platform]}-#{package.metadata[:platform_version]}", :green)
      end
    end
  end
end