summaryrefslogtreecommitdiff
path: root/omnibus/files/chef/build-chef.rb
blob: b0d41c185bb4d02de1393f4755b314b073a6306d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
require "shellwords"
require "pathname"
require "bundler"
require_relative "../chef-gem/build-chef-gem"
require_relative "../../../version_policy"

# We use this to break up the `build` method into readable parts
module BuildChef
  include BuildChefGem

  def create_bundle_config(gemfile, without: without_groups, retries: nil, jobs: nil, frozen: nil)
    bundle_config = File.expand_path("../.bundle/config", gemfile)

    block "Put build config into #{bundle_config}: #{ { without: without, retries: retries, jobs: jobs, frozen: frozen } }" do
      # bundle config build.nokogiri #{nokogiri_build_config} messes up the line,
      # so we write it directly ourselves.
      new_bundle_config = "---\n"
      new_bundle_config << "BUNDLE_WITHOUT: #{Array(without).join(":")}\n" if without
      new_bundle_config << "BUNDLE_RETRY: #{retries}\n" if retries
      new_bundle_config << "BUNDLE_JOBS: #{jobs}\n" if jobs
      new_bundle_config << "BUNDLE_FROZEN: '1'\n" if frozen
      all_install_args.each do |gem_name, install_args|
        new_bundle_config << "BUNDLE_BUILD__#{gem_name.upcase}: #{install_args}\n"
      end
      create_file(bundle_config) { new_bundle_config }
    end
  end

  #
  # Get the (possibly platform-specific) path to the Gemfile.
  #
  def chef_gemfile
    File.join(project_dir, "Gemfile")
  end

  #
  # Some gems we installed don't end up in the `gem list` due to the fact that
  # they have git sources (`gem 'chef', github: 'chef/chef'`) or paths (`gemspec`
  # or `gem 'chef-config', path: 'chef-config'`). To get them in there, we need
  # to go through these gems, run `rake install` from their top level, and
  # then delete the git cached versions.
  #
  # Once we finish with all this, we update the Gemfile that will end up in the
  # chef so that it doesn't have git or path references anymore.
  #
  def properly_reinstall_git_and_path_sourced_gems
    # Emit blank line to separate different tasks
    block { log.info(log_key) { "" } }
    chef_env = env.dup.merge("BUNDLE_GEMFILE" => chef_gemfile)

    # Reinstall git-sourced or path-sourced gems, and delete the originals
    block "Reinstall git-sourced gems properly" do
      # Grab info about the gem environment so we can make decisions
      gemdir = shellout!("#{gem_bin} environment gemdir", env: env).stdout.chomp
      gem_install_dir = File.join(gemdir, "gems")

      # bundle list --paths gets us the list of gem install paths. Get the ones
      # that are installed local (git and path sources like `gem :x, github: 'chef/x'`
      # or `gem :x, path: '.'` or `gemspec`). To do this, we just detect which ones
      # have properly-installed paths (in the `gems` directory that shows up when
      # you run `gem list`).
      locally_installed_gems = shellout!("#{bundle_bin} list --paths", env: chef_env, cwd: project_dir).
        stdout.lines.select { |gem_path| !gem_path.start_with?(gem_install_dir) }

      # Install the gems for real using `rake install` in their directories
      locally_installed_gems.each do |gem_path|
        gem_path = gem_path.chomp
        # We use the already-installed bundle to rake install, because (hopefully)
        # just rake installing doesn't require anything special.
        # Emit blank line to separate different tasks
        log.info(log_key) { "" }
        log.info(log_key) { "Properly installing git or path sourced gem #{gem_path} using rake install" }
        shellout!("#{bundle_bin} exec #{rake_bin} install", env: chef_env, cwd: gem_path)
      end
    end
  end

  def install_shared_gemfile
    # Emit blank line to separate different tasks
    block { log.info(log_key) { "" } }

    shared_gemfile = self.shared_gemfile
    chef_env = env.dup.merge("BUNDLE_GEMFILE" => chef_gemfile)

    # Show the config for good measure
    bundle "config", env: chef_env

    # Make `Gemfile` point to these by removing path and git sources and pinning versions.
    block "Rewrite Gemfile using all properly-installed gems" do
      gem_pins = ""
      result = []
      shellout!("#{bundle_bin} list", env: chef_env).stdout.lines.map do |line|
        if line =~ /^\s*\*\s*(\S+)\s+\((\S+).*\)\s*$/
          name, version = $1, $2
          # rubocop is an exception, since different projects disagree
          next if GEMS_ALLOWED_TO_FLOAT.include?(name)
          gem_pins << "gem #{name.inspect}, #{version.inspect}, override: true\n"
        end
      end

      # Find the installed chef gem by looking for lib/chef.rb
      chef_gem = File.expand_path("../..", shellout!("#{gem_bin} which chef", env: chef_env).stdout.chomp)
      # Figure out the path to gemfile_util from there
      gemfile_util = Pathname.new(File.join(chef_gem, "tasks", "gemfile_util"))
      gemfile_util = gemfile_util.relative_path_from(Pathname.new(shared_gemfile).dirname)

      create_file(shared_gemfile) { <<-EOM }
        # Meant to be included in component Gemfiles at the beginning with:
        #
        #     instance_eval(IO.read("#{install_dir}/Gemfile"), "#{install_dir}/Gemfile")
        #
        # Override any existing gems with our own.
        require_relative "#{gemfile_util}"
        extend GemfileUtil
        #{gem_pins}
      EOM
    end

    shared_gemfile_env = env.dup.merge("BUNDLE_GEMFILE" => shared_gemfile)

    # Create a `Gemfile.lock` at the final location
    bundle "lock", env: shared_gemfile_env

    # Freeze the location's Gemfile.lock.
    # TODO Windows cannot be frozen, because Bundler doesn't understand platform-specific
    # versions. However, on Windows we have explicit version pins for most things, so
    # we will *probably* get the exact versions of everything we want.
    create_bundle_config(shared_gemfile, frozen: !windows?)

    # Clear the now-unnecessary git caches, cached gems, and git-checked-out gems
    block "Delete bundler git cache and git installs" do
      gemdir = shellout!("#{gem_bin} environment gemdir", env: env).stdout.chomp
      remove_file "#{gemdir}/cache"
      remove_file "#{gemdir}/bundler"
    end
  end
end