#
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# 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.
#

# This component builds all gems that we distribute as part of chef-workstation.
name "gems"
source path: File.join("#{project.files_path}", "../../components/gems")
license :project_license

dependency "ruby"

#
# NOTE: Do not add any gem dependencies here.  This will cause gemsets to solve without
# the full constrains of chef-workstation, which can result in multiple gem versions
# shipping in the omnibus package.

# However, for gems that depend on c-libs, we must include the c-libraries directly here.

# For nokogiri, archive_file in chef infra client, and berkshelf
dependency "libxml2"
dependency "libxslt"
dependency "liblzma"
dependency "zlib"
dependency "libarchive"

# This is a transative dep but we need to build from source so binaries are built on current sdk.
# Only matters on mac.
# @todo https://github.com/guard/rb-fsevent/issues/83
dependency "rb-fsevent-gem" if macos?

build do
  env = if !windows?
          with_standard_compiler_flags(with_embedded_path)
        else
          # On Windows the `gems` build runs BEFORE `ruby-msys2-devkit` lands an
          # embedded copy of msys2 under `embedded\msys64`, so we cannot provide
          # gcc/make from the embedded tree yet. We must rely on the host msys2
          # (`C:\msys64`) provided by the omnibus-toolchain image for native gem
          # extension builds.
          #
          # However, leaving the runner's full PATH in place also exposes
          # `C:\Program Files\Git` and
          # `C:\opscode\omnibus-toolchain\embedded\bin`, both of which ship a
          # foreign `sh.exe` that beats the embedded git's bash on PATH. When
          # `git.exe` (called by Bundler to fetch git-source gems) forks
          # `git-submodule.sh` under that foreign `sh.exe`, `. git-sh-setup`
          # fails with:
          #
          #   git-submodule: line 22: .: git-sh-setup: file not found
          #
          # Build PATH from the embedded toolchain + host msys2 only, explicitly
          # excluding Git for Windows and omnibus-toolchain.
          #
          # `embedded\git\mingw64\bin` is intentionally NOT on PATH: it ships
          # older `libstdc++-6.dll` / `libgcc_s_seh-1.dll` / `libwinpthread-1.dll`
          # that get loaded by the host msys2 `ld.exe` and trigger
          # `collect2.exe: error: ld returned 116 exit status` when linking
          # native gem extensions like `libyajl2`. `git.exe` lives in
          # `embedded\git\cmd`; the bash used by `git-submodule.sh` lives in
          # `embedded\git\usr\bin\sh.exe`; git's internal subcommands live in
          # `embedded\git\mingw64\libexec\git-core`. None of those require
          # `mingw64\bin` on PATH.
          host_msys2 = ENV["MSYS2_INSTALL_DIR"] || 'C:\msys64'
          embedded_path = [
            "#{install_dir}\\embedded\\bin",
            "#{install_dir}\\embedded\\git\\cmd",
            "#{install_dir}\\embedded\\git\\mingw64\\libexec\\git-core",
            "#{install_dir}\\embedded\\git\\usr\\bin",
            "#{host_msys2}\\ucrt64\\bin",
            "#{host_msys2}\\usr\\bin",
            'C:\Windows\system32',
            'C:\Windows',
            'C:\Windows\System32\Wbem',
            'C:\Windows\System32\WindowsPowerShell\v1.0',
          ].join(";")
          { "Path" => embedded_path }
        end

  #######################################################
  # !!!              IMPORTANT REMINDER             !!! #
  #######################################################
  # Any changes to the gem component ('without' flags,  #
  # additions/removals) must also be updated in in      #
  # habitat/plan.sh                                     #
  #######################################################

  # TODO: - reevaluate groups - would like to universally exclude development,
  # testing but we'll need to make sure that's safe and doesn't remove
  # gems that the various applications need for day-to-day functionality.
  excluded_groups = %w{server docgen maintenance pry travis integration ci}

  if macos?
    env["NOKOGIRI_LIBXML2_PATCH_VERSION"] = "14"

    # rb-fsevent is prebuilt from source on macOS via the rb-fsevent-gem
    # dependency (so the shipped fsevent_watch binary is our own, notarizable
    # build). Two git-cache hazards to handle before `bundle install`:
    #
    # 1. When this component rebuilds (e.g. the Gemfile.lock changed) on top of
    #    a git-cache-restored install dir, the restore can leave rb-fsevent
    #    half-installed: its gemspec present but the gem payload gone. Bundler
    #    then insists on reinstalling it, and because the matched spec is local
    #    it never falls back to rubygems.org -- it reads the local gem cache
    #    only. Heal the install from the cached .gem that rb-fsevent-gem left
    #    behind; without one, drop the orphaned spec so bundler does a clean
    #    remote install instead of aborting with Gem::MissingSpecError.
    # 2. Remove the cached .gem afterwards so Bundler does not compare the
    #    locally-built package checksum against Rubygems API checksums.
    block "Heal locally built rb-fsevent and drop its cached .gem" do
      gembin = "#{install_dir}/embedded/bin/gem"
      gemdir = shellout!("#{gembin} environment gemdir", env: env).stdout.chomp

      cached  = Dir.glob("#{gemdir}/cache/rb-fsevent-*.gem").max
      spec    = Dir.glob("#{gemdir}/specifications/rb-fsevent-*.gemspec").max
      payload = Dir.glob("#{gemdir}/gems/rb-fsevent-*").max

      unless spec && payload
        if cached
          puts "Reinstalling locally built rb-fsevent (incomplete git-cache restore)"
          shellout!("#{gembin} install #{cached} --local --force --no-document --ignore-dependencies", env: env)
        else
          puts "WARNING: locally built rb-fsevent is gone and no cached .gem remains; " \
               "bundler will install the upstream gem (prebuilt fsevent_watch)"
          [spec, payload].compact.each { |leftover| shellout!("rm -rf #{leftover}", env: env) }
        end
      end

      shellout!("rm -f #{gemdir}/cache/rb-fsevent-*.gem", env: env)
    end
  else
    env["NOKOGIRI_USE_SYSTEM_LIBRARIES"] = "true"
  end

  # install the whole bundle first
  bundle "config set --local without '#{excluded_groups.join(" ")}'", env: env

  # Bundler's parallel installer races on Windows native gem links. With
  # `--jobs 10` the libyajl2 link step intermittently dies with
  # `collect2.exe: error: ld returned 116 exit status` and zero linker
  # output, even with the embedded\git\mingw64\bin PATH workaround in
  # place. Forcing serial install on Windows trades a few minutes of
  # build time for a reliable build.
  install_jobs = windows? ? 1 : 10
  bundle "install --jobs #{install_jobs}", env: env

  # Heal mixlib-versioning into the shipped gem set. It is reached in the
  # product bundle only through the source-pinned mixlib-install, and it is
  # ALSO a build-time dependency of omnibus itself -- so `bundle install` for
  # the build tooling installs it into bundle/vendor first. On the arm64-darwin
  # builders that copy is on GEM_PATH when the product `bundle install` runs, so
  # bundler treats it as already satisfied and never installs it into the
  # embedded gem set that actually ships. The package then omits it and every
  # tool that loads mixlib-install (cinc-auditor, knife, ohai, ...) dies with
  # `Could not find 'mixlib-versioning' (Gem::MissingSpecError)`. Force it into
  # the embedded gems if bundler skipped it. Keep the version in sync with the
  # components/gems Gemfile.lock resolution.
  block "Ensure mixlib-versioning ships in the embedded gems" do
    gembin = "#{install_dir}/embedded/bin/gem"
    gemdir = shellout!("#{gembin} environment gemdir", env: env).stdout.chomp
    if Dir.glob("#{gemdir}/specifications/mixlib-versioning-*.gemspec").empty?
      puts "mixlib-versioning missing from the embedded gems (bundler resolved it " \
           "from the build bundle); installing it directly"
      shellout!("#{gembin} install mixlib-versioning --version 1.2.12 " \
                "--install-dir #{gemdir} --no-document --ignore-dependencies", env: env)
    end
  end

  ruby "post-bundle-install.rb", env: env

  appbundle "knife", lockdir: project_dir, gem: "knife", without: %w{development}, env: env
  appbundle "test-kitchen", lockdir: project_dir, gem: "test-kitchen",
    without: %w{changelog debug docs development integration}, env: env
  appbundle "inspec", lockdir: project_dir, gem: "cinc-auditor-bin", without: %w{deploy tools maintenance integration},
    env: env
  appbundle "chef-cli", lockdir: project_dir, gem: "chef-cli", without: %w{changelog profile test}, env: env
  appbundle "berkshelf", lockdir: project_dir, gem: "berkshelf", without: %w{changelog build docs debug development},
    env: env
  appbundle "mixlib-install", lockdir: project_dir, gem: "mixlib-install", without: %w{test chefstyle debug}, env: env
  appbundle "chef-zero", lockdir: project_dir, gem: "chef-zero", without: %w{pedant development debug}, env: env
  appbundle "cookstyle", lockdir: project_dir, gem: "cookstyle",
    without: %w{docs profiling rubocop_gems development debug}, env: env
  appbundle "fauxhai", lockdir: project_dir, gem: "fauxhai-chef", env: env
  appbundle "taste_tester", lockdir: project_dir, gem: "taste_tester", without: %w{test development}, env: env

  %w{chef-bin chef-vault ohai}.each do |gem|
    appbundle gem, lockdir: project_dir, gem: gem, without: %w{changelog development}, env: env
  end

  # Preserve the bundler/gems directory on all platforms: license_scout runs at
  # project end and needs to resolve git-sourced gems (ruby-shadow, kitchen-sync,
  # taste_tester) from the Gemfile.lock. Deleting it here caused license_scout
  # to abort on the first git gem, leaving the bundled LICENSES manifest with
  # only system-library entries. See cinc-workstation#9.
end
