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
|