-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
gems.rb
353 lines (293 loc) 路 11 KB
/
gems.rb
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# typed: true
# frozen_string_literal: true
# Never `require` anything in this file (except English). It needs to be able to
# work as the first item in `brew.rb` so we can load gems with Bundler when
# needed before anything else is loaded (e.g. `json`).
require "English"
module Homebrew
# Keep in sync with the `Gemfile.lock`'s BUNDLED WITH.
# After updating this, run `brew vendor-gems --update=--bundler`.
HOMEBREW_BUNDLER_VERSION = "2.4.18"
# Bump this whenever a committed vendored gem is later added to gitignore.
# This will trigger it to reinstall properly if `brew install-bundler-gems` needs it.
VENDOR_VERSION = 1
private_constant :VENDOR_VERSION
RUBY_BUNDLE_VENDOR_DIRECTORY = (HOMEBREW_LIBRARY_PATH/"vendor/bundle/ruby").freeze
private_constant :RUBY_BUNDLE_VENDOR_DIRECTORY
# This is tracked across Ruby versions.
GEM_GROUPS_FILE = (RUBY_BUNDLE_VENDOR_DIRECTORY/".homebrew_gem_groups").freeze
private_constant :GEM_GROUPS_FILE
# This is tracked per Ruby version.
VENDOR_VERSION_FILE = (
RUBY_BUNDLE_VENDOR_DIRECTORY/"#{RbConfig::CONFIG["ruby_version"]}/.homebrew_vendor_version"
).freeze
private_constant :VENDOR_VERSION_FILE
module_function
# @api private
def gemfile
File.join(ENV.fetch("HOMEBREW_LIBRARY"), "Homebrew", "Gemfile")
end
# @api private
def bundler_definition
@bundler_definition ||= Bundler::Definition.build(Bundler.default_gemfile, Bundler.default_lockfile, false)
end
# @api private
def valid_gem_groups
install_bundler!
require "bundler"
Bundler.with_unbundled_env do
ENV["BUNDLE_GEMFILE"] = gemfile
groups = bundler_definition.groups
groups.delete(:default)
groups.map(&:to_s)
end
end
def ruby_bindir
"#{RbConfig::CONFIG["prefix"]}/bin"
end
def ohai_if_defined(message)
if defined?(ohai)
$stderr.ohai message
else
$stderr.puts "==> #{message}"
end
end
def opoo_if_defined(message)
if defined?(opoo)
$stderr.opoo message
else
$stderr.puts "Warning: #{message}"
end
end
def odie_if_defined(message)
if defined?(odie)
odie message
else
$stderr.puts "Error: #{message}"
exit 1
end
end
def setup_gem_environment!(setup_path: true)
require "rubygems"
raise "RubyGems too old!" if Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.2.0")
ENV["BUNDLER_NO_OLD_RUBYGEMS_WARNING"] = "1"
# Match where our bundler gems are.
gem_home = "#{RUBY_BUNDLE_VENDOR_DIRECTORY}/#{RbConfig::CONFIG["ruby_version"]}"
Gem.paths = {
"GEM_HOME" => gem_home,
"GEM_PATH" => gem_home,
}
# Set TMPDIR so Xcode's `make` doesn't fall back to `/var/tmp/`,
# which may be not user-writable.
ENV["TMPDIR"] = ENV.fetch("HOMEBREW_TEMP", nil)
return unless setup_path
# Add necessary Ruby and Gem binary directories to `PATH`.
paths = ENV.fetch("PATH").split(":")
paths.unshift(ruby_bindir) unless paths.include?(ruby_bindir)
paths.unshift(Gem.bindir) unless paths.include?(Gem.bindir)
ENV["PATH"] = paths.compact.join(":")
# Set envs so the above binaries can be invoked.
# We don't do this unless requested as some formulae may invoke system Ruby instead of ours.
ENV["GEM_HOME"] = gem_home
ENV["GEM_PATH"] = gem_home
end
def install_gem!(name, version: nil, setup_gem_environment: true)
setup_gem_environment! if setup_gem_environment
specs = Gem::Specification.find_all_by_name(name, version)
if specs.empty?
ohai_if_defined "Installing '#{name}' gem"
# `document: []` is equivalent to --no-document
# `build_args: []` stops ARGV being used as a default
# `env_shebang: true` makes shebangs generic to allow switching between system and Portable Ruby
specs = Gem.install name, version, document: [], build_args: [], env_shebang: true
end
specs += specs.flat_map(&:runtime_dependencies)
.flat_map(&:to_specs)
# Add the specs to the $LOAD_PATH.
specs.each do |spec|
spec.require_paths.each do |path|
full_path = File.join(spec.full_gem_path, path)
$LOAD_PATH.unshift full_path unless $LOAD_PATH.include?(full_path)
end
end
rescue Gem::UnsatisfiableDependencyError
odie_if_defined "failed to install the '#{name}' gem."
end
def install_gem_setup_path!(name, version: nil, executable: name, setup_gem_environment: true)
install_gem!(name, version: version, setup_gem_environment: setup_gem_environment)
return if find_in_path(executable)
odie_if_defined <<~EOS
the '#{name}' gem is installed but couldn't find '#{executable}' in the PATH:
#{ENV.fetch("PATH")}
EOS
end
def find_in_path(executable)
ENV.fetch("PATH").split(":").find do |path|
File.executable?(File.join(path, executable))
end
end
def install_bundler!
old_bundler_version = ENV.fetch("BUNDLER_VERSION", nil)
setup_gem_environment!
ENV["BUNDLER_VERSION"] = HOMEBREW_BUNDLER_VERSION # Set so it correctly finds existing installs
install_gem_setup_path!(
"bundler",
version: HOMEBREW_BUNDLER_VERSION,
executable: "bundle",
setup_gem_environment: false,
)
ensure
ENV["BUNDLER_VERSION"] = old_bundler_version
end
def user_gem_groups
@user_gem_groups ||= if GEM_GROUPS_FILE.exist?
GEM_GROUPS_FILE.readlines(chomp: true)
elsif RUBY_VERSION < "2.7"
# Backwards compatibility. This elsif block removed by the end of 2023.
# We will not support this in Ruby >=2.7.
require "settings"
groups = Homebrew::Settings.read(:gemgroups)&.split(";") || []
write_user_gem_groups(groups)
Homebrew::Settings.delete(:gemgroups)
groups
else
[]
end
end
def write_user_gem_groups(groups)
GEM_GROUPS_FILE.write(groups.join("\n"))
end
def forget_user_gem_groups!
if GEM_GROUPS_FILE.exist?
GEM_GROUPS_FILE.truncate(0)
elsif RUBY_VERSION < "2.7"
# Backwards compatibility. This else block can be removed by the end of 2023.
# We will not support this in Ruby >=2.7.
require "settings"
Homebrew::Settings.delete(:gemgroups)
end
end
def user_vendor_version
@user_vendor_version ||= if VENDOR_VERSION_FILE.exist?
VENDOR_VERSION_FILE.read.to_i
else
0
end
end
def install_bundler_gems!(only_warn_on_failure: false, setup_path: true, groups: [])
old_path = ENV.fetch("PATH", nil)
old_gem_path = ENV.fetch("GEM_PATH", nil)
old_gem_home = ENV.fetch("GEM_HOME", nil)
old_bundle_gemfile = ENV.fetch("BUNDLE_GEMFILE", nil)
old_bundle_with = ENV.fetch("BUNDLE_WITH", nil)
old_bundle_frozen = ENV.fetch("BUNDLE_FROZEN", nil)
old_sdkroot = ENV.fetch("SDKROOT", nil)
invalid_groups = groups - valid_gem_groups
raise ArgumentError, "Invalid gem groups: #{invalid_groups.join(", ")}" unless invalid_groups.empty?
# tests should not modify the state of the repo
if ENV["HOMEBREW_TESTS"]
setup_gem_environment!
return
end
install_bundler!
valid_user_gem_groups = user_gem_groups & valid_gem_groups
if RUBY_PLATFORM.end_with?("-darwin23")
raise "Sorbet is not currently supported under system Ruby on macOS Sonoma." if groups.include?("sorbet")
valid_user_gem_groups.delete("sorbet")
end
# Combine the passed groups with the ones stored in settings
groups |= valid_user_gem_groups
groups.sort!
ENV["BUNDLE_GEMFILE"] = gemfile
ENV["BUNDLE_WITH"] = groups.join(" ")
ENV["BUNDLE_FROZEN"] = "true"
# System Ruby does not pick up the correct SDK by default.
if ENV["HOMEBREW_MACOS_SYSTEM_RUBY_NEW_ENOUGH"]
macos_major = ENV.fetch("HOMEBREW_MACOS_VERSION").partition(".").first
sdkroot = "/Library/Developer/CommandLineTools/SDKs/MacOSX#{macos_major}.sdk"
ENV["SDKROOT"] = sdkroot if Dir.exist?(sdkroot)
end
if @bundle_installed_groups != groups
bundle = File.join(find_in_path("bundle"), "bundle")
bundle_check_output = `#{bundle} check 2>&1`
bundle_check_failed = !$CHILD_STATUS.success?
# for some reason sometimes the exit code lies so check the output too.
bundle_install_required = bundle_check_failed || bundle_check_output.include?("Install missing gems")
if user_vendor_version != VENDOR_VERSION
# Check if the install is intact. This is useful if any gems are added to gitignore.
# We intentionally map over everything and then call `any?` so that we remove the spec of each bad gem.
specs = bundler_definition.resolve.materialize(bundler_definition.locked_dependencies)
vendor_reinstall_required = specs.map do |spec|
spec_file = "#{Gem.dir}/specifications/#{spec.full_name}.gemspec"
next false unless File.exist?(spec_file)
cache_file = "#{Gem.dir}/cache/#{spec.full_name}.gem"
if File.exist?(cache_file)
require "rubygems/package"
package = Gem::Package.new(cache_file)
package_install_intact = begin
contents = package.contents
# If the gem has contents, ensure we have every file installed it contains.
contents&.all? do |gem_file|
File.exist?("#{Gem.dir}/gems/#{spec.full_name}/#{gem_file}")
end
rescue Gem::Package::Error, Gem::Security::Exception
# Malformed, assume broken
File.unlink(cache_file)
false
end
next false if package_install_intact
end
# Mark gem for reinstallation
File.unlink(spec_file)
true
end.any?
VENDOR_VERSION_FILE.dirname.mkpath
VENDOR_VERSION_FILE.write(VENDOR_VERSION.to_s)
bundle_install_required ||= vendor_reinstall_required
end
bundle_installed = if bundle_install_required
if system bundle, "install", out: :err
true
else
message = <<~EOS
failed to run `#{bundle} install`!
EOS
if only_warn_on_failure
opoo_if_defined message
else
odie_if_defined message
end
false
end
elsif system bundle, "clean", out: :err # even if we have nothing to install, we may have removed gems
true
else
message = <<~EOS
failed to run `#{bundle} clean`!
EOS
if only_warn_on_failure
opoo_if_defined message
else
odie_if_defined message
end
false
end
if bundle_installed
write_user_gem_groups(groups)
@bundle_installed_groups = groups
end
end
setup_gem_environment!
ensure
unless setup_path
# Reset the paths. We need to have at least temporarily changed them while invoking `bundle`.
ENV["PATH"] = old_path
ENV["GEM_PATH"] = old_gem_path
ENV["GEM_HOME"] = old_gem_home
ENV["BUNDLE_GEMFILE"] = old_bundle_gemfile
ENV["BUNDLE_WITH"] = old_bundle_with
ENV["BUNDLE_FROZEN"] = old_bundle_frozen
end
ENV["SDKROOT"] = old_sdkroot
end
end