-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathcreate_bundle_build_file.rb
More file actions
executable file
·379 lines (309 loc) · 13.1 KB
/
create_bundle_build_file.rb
File metadata and controls
executable file
·379 lines (309 loc) · 13.1 KB
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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#!/usr/bin/env ruby
# frozen_string_literal: true
BUILD_HEADER = <<~MAIN_TEMPLATE
load(
"{workspace_name}//ruby:defs.bzl",
"ruby_library",
)
package(default_visibility = ["//visibility:public"])
ruby_library(
name = "bundler_setup",
srcs = ["lib/bundler/setup.rb"],
visibility = ["//visibility:private"],
)
ruby_library(
name = "bundler",
srcs = glob(
include = [
"bundler/**/*",
],
),
)
# PULL EACH GEM INDIVIDUALLY
MAIN_TEMPLATE
GEM_TEMPLATE = <<~GEM_TEMPLATE
ruby_library(
name = "{name}",
srcs = glob(
include = [
".bundle/config",
{gem_lib_files},
"{gem_spec}",
{gem_binaries}
],
exclude = {exclude},
),
deps = {deps},
includes = [{gem_lib_paths}],
)
GEM_TEMPLATE
GEM_GROUP = <<~GEM_GROUP
ruby_library(
name = "gems_{group_name}_group",
deps = {group_gems}
)
GEM_GROUP
ALL_GEMS = <<~ALL_GEMS
ruby_library(
name = "gems",
srcs = glob([{bundle_lib_files}]) + glob(["bin/*"]),
includes = {bundle_lib_paths},
)
ruby_library(
name = "bin",
srcs = glob(["bin/*"]),
deps = {bundle_with_binaries}
)
ALL_GEMS
# For ordinary gems, this path is like 'lib/ruby/3.0.0/gems/rspec-3.10.0'.
# For gems with native extension installed via prebuilt packages, the last part of this path can
# contain an OS-specific suffix like 'grpc-1.38.0-universal-darwin' or 'grpc-1.38.0-x86_64-linux'
# instead of 'grpc-1.38.0'.
#
# Since OS platform is unlikely to change between Bazel builds on the same machine,
# `#{gem_name}-#{gem_version}*` would be sufficient to narrow down matches to at most one.
#
# Library path differs across implementations as `lib/ruby` on MRI and `lib/jruby` on JRuby.
GEM_PATH = ->(ruby_version, gem_name, gem_version) do
Dir.glob("lib/#{RbConfig::CONFIG['RUBY_INSTALL_NAME']}/#{ruby_version}/gems/#{gem_name}-#{gem_version}*").first
end
# For ordinary gems, this path is like 'lib/ruby/3.0.0/specifications/rspec-3.10.0.gemspec'.
# For gems with native extension installed via prebuilt packages, the last part of this path can
# contain an OS-specific suffix like 'grpc-1.38.0-universal-darwin.gemspec' or
# 'grpc-1.38.0-x86_64-linux.gemspec' instead of 'grpc-1.38.0.gemspec'.
#
# Since OS platform is unlikely to change between Bazel builds on the same machine,
# `#{gem_name}-#{gem_version}*.gemspec` would be sufficient to narrow down matches to at most one.
#
# Library path differs across implementations as `lib/ruby` on MRI and `lib/jruby` on JRuby.
SPEC_PATH = ->(ruby_version, gem_name, gem_version) do
Dir.glob("lib/#{RbConfig::CONFIG['RUBY_INSTALL_NAME']}/#{ruby_version}/specifications/#{gem_name}-#{gem_version}*.gemspec").first
end
EXTENSIONS_PATH = ->(ruby_version, gem_name, gem_version) do
Dir.glob("lib/#{RbConfig::CONFIG['RUBY_INSTALL_NAME']}/#{ruby_version}/extensions/*/#{gem_name}/**/*").first
end
require 'bundler'
require 'json'
require 'stringio'
require 'fileutils'
require 'tempfile'
# colorization
class String
def colorize(color_code)
"\e[#{color_code}m#{self}\e[0m"
end
# @formatter:off
def red; colorize(31); end
def green; colorize(32); end
def yellow; colorize(33); end
def blue; colorize(34); end
def pink; colorize(35); end
def light_blue; colorize(36); end
def orange; colorize(52); end
# @formatter:on
end
class Buildifier
attr_reader :build_file, :output_file
# @formatter:off
class BuildifierError < StandardError; end
class BuildifierNotFoundError < BuildifierError; end
class BuildifierFailedError < BuildifierError; end
class BuildifierNoBuildFileError < BuildifierError; end
# @formatter:on
def initialize(build_file)
@build_file = build_file
# For capturing buildifier output
@output_file = ::Tempfile.new("/tmp/#{File.dirname(File.absolute_path(build_file))}/#{build_file}.stdout").path
end
def buildify!
raise BuildifierNoBuildFileError, 'Can\'t find the BUILD file' unless File.exist?(build_file)
# see if we can find buildifier on the filesystem
buildifier = `bash -c 'command -v buildifier'`.strip
raise BuildifierNotFoundError, 'Can\'t find buildifier' unless buildifier && File.executable?(buildifier)
command = "#{buildifier} -v #{File.absolute_path(build_file)}"
system("/usr/bin/env bash -c '#{command} 1>#{output_file} 2>&1'")
code = $?
return unless File.exist?(output_file)
output = File.read(output_file).strip.gsub(Dir.pwd, '.').yellow
begin
FileUtils.rm_f(output_file)
rescue StandardError
nil
end
if code == 0
puts 'Buildifier gave 👍 '.green + (output ? " and said: #{output}" : '')
else
raise BuildifierFailedError,
"Generated BUILD file failed buildifier, with error:\n\n#{output.yellow}\n\n".red
end
end
end
class BundleBuildFileGenerator
attr_reader :workspace_name,
:repo_name,
:build_file,
:gemfile_lock,
:includes,
:excludes,
:ruby_version
DEFAULT_EXCLUDES = ['**/* *.*', '**/* */*'].freeze
EXCLUDED_EXECUTABLES = %w(console setup).freeze
def initialize(workspace_name:,
repo_name:,
build_file: 'BUILD.bazel',
gemfile_lock: 'Gemfile.lock',
includes: nil,
excludes: nil)
@workspace_name = workspace_name
@repo_name = repo_name
@build_file = build_file
@gemfile_lock = gemfile_lock
@includes = includes
@excludes = excludes
# This attribute returns 0 as the third minor version number, which happens to be
# what Ruby uses in the PATH to gems, eg. ruby 2.6.5 would have a folder called
# ruby/2.6.0/gems for all minor versions of 2.6.*
@ruby_version ||= (RUBY_VERSION.split('.')[0..1] << 0).join('.')
end
def generate!
# when we append to a string many times, using StringIO is more efficient.
template_out = StringIO.new
template_out.puts BUILD_HEADER
.gsub('{workspace_name}', workspace_name)
.gsub('{repo_name}', repo_name)
.gsub('{ruby_version}', ruby_version)
.gsub('{bundler_setup}', bundler_setup_require)
# strip bundler version so we can process this file
remove_bundler_version!
# Append to the end specific gem libraries and dependencies
bundle = Bundler::LockfileParser.new(Bundler.read_file(gemfile_lock))
bundle_lib_paths = []
bundle_binaries = {} # gem-name => [ gem's binaries ], ...
gems = bundle.specs.map(&:name)
bundle_deps = Bundler::Definition.build(gemfile_lock.chomp('.lock'), gemfile_lock, {}).dependencies
groups = bundle_deps.map{|dep| dep.groups}.flatten.uniq
gems_by_group = groups.map{ |g| {g => bundle_deps
.select{|dep| dep.groups.include?(g)}
.reject{|dep| dep.source.path? unless dep.source.nil?}
.map(&:name)}
}
.reduce Hash.new, :merge
bundle.specs.each { |spec| register_gem(spec, template_out, bundle_lib_paths, bundle_binaries) }
template_out.puts ALL_GEMS
.gsub('{bundle_lib_files}', to_flat_string(bundle_lib_paths.map { |p| "#{p}/**/*" }))
.gsub('{bundle_with_binaries}', bundle_binaries.keys.map { |g| ":#{g}" }.to_s)
.gsub('{bundle_binaries}', bundle_binaries.values.flatten.to_s)
.gsub('{bundle_lib_paths}', bundle_lib_paths.to_s)
.gsub('{bundler_setup}', bundler_setup_require)
.gsub('{bundle_deps}', gems.map { |g| ":#{g}" }.to_s)
.gsub('{exclude}', DEFAULT_EXCLUDES.to_s)
gems_by_group.each do |key, value|
template_out.puts GEM_GROUP
.gsub('{group_name}', key.to_s)
.gsub('{group_gems}', value.map{|s| s.prepend ':' unless s.start_with? ':'}.compact.to_s)
end
::File.open(build_file, 'w') { |f| f.puts template_out.string }
end
private
def bundler_setup_require
@bundler_setup_require ||= "-r#{runfiles_path('lib/bundler/setup.rb')}"
end
def runfiles_path(path)
"${RUNFILES_DIR}/#{repo_name}/#{path}"
end
# This method scans the contents of the Gemfile.lock and if it finds BUNDLED WITH
# it strips that line + the line below it, so that any version of bundler would work.
def remove_bundler_version!
contents = File.read(gemfile_lock)
return unless contents =~ /BUNDLED WITH/
temp_gemfile_lock = "#{gemfile_lock}.no-bundle-version"
system %(sed -n '/BUNDLED WITH/q;p' "#{gemfile_lock}" > #{temp_gemfile_lock})
::FileUtils.rm_f(gemfile_lock) if File.symlink?(gemfile_lock) # it's just a symlink
::FileUtils.move(temp_gemfile_lock, gemfile_lock, force: true)
end
def register_gem(spec, template_out, bundle_lib_paths, bundle_binaries)
# Do not register local gems
return if spec.source.path?
base_dir = "lib/ruby/#{ruby_version}"
if spec.source.is_a?(Bundler::Source::Git)
stub = spec.source.specs.find { |s| s.name == spec.name }.stub
gem_path = "#{base_dir}/bundler/gems/#{Pathname(stub.full_gem_path).relative_path_from(stub.base_dir)}"
spec_path = "#{base_dir}/bundler/gems/#{Pathname(stub.loaded_from).relative_path_from(stub.base_dir)}"
# paths to register to $LOAD_PATH
require_paths = stub.require_paths
else
gem_path = GEM_PATH[ruby_version, spec.name, spec.version]
spec_path = SPEC_PATH[ruby_version, spec.name, spec.version]
# paths to register to $LOAD_PATH
require_paths = Gem::StubSpecification.gemspec_stub(spec_path, base_dir, "#{base_dir}/gems").require_paths
end
# Usually, registering the directory paths listed in the `require_paths` of gemspecs is sufficient, but
# some gems also require additional paths to be included in the load paths.
require_paths += include_array(spec.name)
gem_lib_paths = require_paths.map { |require_path| File.join(gem_path, require_path) }
bundle_lib_paths.push(*gem_lib_paths)
# paths to search for executables
gem_binaries = find_bundle_binaries(gem_path)
bundle_binaries[spec.name] = gem_binaries unless gem_binaries.nil? || gem_binaries.empty?
deps = spec.dependencies.map { |d| ":#{d.name}" }
warn("registering gem #{spec.name} with binaries: #{gem_binaries}") if bundle_binaries.key?(spec.name)
template_out.puts GEM_TEMPLATE
.gsub('{gem_lib_paths}', to_flat_string(gem_lib_paths))
.gsub('{gem_lib_files}', to_flat_string(gem_lib_paths.map { |p| "#{p}/**/*" }))
.gsub('{gem_spec}', spec_path)
.gsub('{gem_binaries}', to_flat_string(gem_binaries))
.gsub('{exclude}', exclude_array(spec.name).to_s)
.gsub('{name}', spec.name)
.gsub('{version}', spec.version.to_s)
.gsub('{deps}', deps.to_s)
.gsub('{repo_name}', repo_name)
.gsub('{ruby_version}', ruby_version)
.gsub('{bundler_setup}', bundler_setup_require)
end
def find_bundle_binaries(gem_path)
gem_bin_paths = %W(#{gem_path}/bin #{gem_path}/exe)
gem_bin_paths
.map do |bin_path|
Dir # grab all files under bin/ and exe/ inside the gem folder
.glob("#{bin_path}/*") # convert to File object
.map { |b| f = File.new(b); File.executable?(f) ? f : nil }
.compact # remove non-executables, take basename, minus binary defaults
.map { |f| File.basename(f.path) } - EXCLUDED_EXECUTABLES # that bundler installs with bundle gem <name
end.flatten
.compact
.sort
.map { |binary| "bin/#{binary}" }
end
def gems_in_group(gems, group_name)
gems
end
def include_array(gem_name)
(includes[gem_name] || [])
end
def exclude_array(gem_name)
(excludes[gem_name] || []) + DEFAULT_EXCLUDES
end
def to_flat_string(array)
array.to_s.gsub(/[\[\]]/, '')
end
end
# ruby ./create_bundle_build_file.rb "BUILD.bazel" "Gemfile.lock" "repo_name" "{}" "{}" "wsp_name"
if $0 == __FILE__
if ARGV.length != 6
warn("USAGE: #{$0} BUILD.bazel Gemfile.lock repo-name {includes-json} {excludes-json} workspace-name".orange)
exit(1)
end
build_file, gemfile_lock, repo_name, includes, excludes, workspace_name, * = *ARGV
BundleBuildFileGenerator.new(build_file: build_file,
gemfile_lock: gemfile_lock,
repo_name: repo_name,
includes: JSON.parse(includes),
excludes: JSON.parse(excludes),
workspace_name: workspace_name).generate!
begin
Buildifier.new(build_file).buildify!
puts("Buildifier successful on file #{build_file} ")
rescue Buildifier::BuildifierError => e
warn("ERROR running buildifier on the generated build file [#{build_file}] ➔ #{e.message.orange}")
end
end