Class: Jets::Builders::CodeBuilder

Inherits:
Object
  • Object
show all
Includes:
ActionView::Helpers::NumberHelper, Timing
Defined in:
lib/jets/builders/code_builder.rb

Constant Summary

Constants included from Timing

Timing::RECORD_LOG_PATH

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Timing

clear, #record_data, #record_log, report

Constructor Details

#initializeCodeBuilder

Returns a new instance of CodeBuilder.



58
59
60
61
62
# File 'lib/jets/builders/code_builder.rb', line 58

def initialize
  # Expanding to the full path and capture now.
  # Dir.chdir gets called later and we'll lose this info.
  @full_project_path = File.expand_path(Jets.root) + "/"
end

Instance Attribute Details

#full_project_pathObject (readonly)

Returns the value of attribute full_project_path.



57
58
59
# File 'lib/jets/builders/code_builder.rb', line 57

def full_project_path
  @full_project_path
end

Class Method Details

.tmp_app_rootObject

Group all the path settings together here



447
448
449
# File 'lib/jets/builders/code_builder.rb', line 447

def self.tmp_app_root
  Jets::Commands::Build.tmp_app_root
end

Instance Method Details

#buildObject



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/jets/builders/code_builder.rb', line 64

def build
  return create_zip_file(fake=true) if ENV['TEST_CODE'] # early return

  cache_check_message
  check_ruby_version

  clean_start
  compile_assets # easier to do before we copy the project
  copy_project
  Dir.chdir(full(tmp_app_root)) do
    # These commands run from project root
    start_app_root_setup
    bundle
    finish_app_root_setup
    create_zip_file
  end
end

#bundleObject



305
306
307
308
# File 'lib/jets/builders/code_builder.rb', line 305

def bundle
  clean_old_submodules
  bundle_install
end

#bundle_installObject

Installs gems on the current target system: both compiled and non-compiled. If user is on a macosx machine, macosx gems will be installed. If user is on a linux machine, linux gems will be installed.

Copies Gemfile* to /tmp/jetss/demo/bundled folder and installs gems with bundle install from there.

We take the time to copy Gemfile and bundle into a separate directory because it gets left around to act as a ‘cache’. So, when the builds the project gets built again not all the gems from get installed from the beginning.



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/jets/builders/code_builder.rb', line 322

def bundle_install
  return if poly_only?

  headline "Bundling: running bundle install in cache area: #{cache_area}."

  copy_gemfiles

  require "bundler" # dynamically require bundler so user can use any bundler
  Bundler.with_clean_env do
    # cd /tmp/jets/demo
    sh(
      "cd #{cache_area} && " \
      "env BUNDLE_IGNORE_CONFIG=1 bundle install --path bundled/gems --without development test"
    )
  end

  puts 'Bundle install success.'
end

#cache_areaObject



436
437
438
# File 'lib/jets/builders/code_builder.rb', line 436

def cache_area
  "#{Jets.build_root}/cache" # cleaner to use full path for this setting
end

#cache_check_messageObject



412
413
414
415
416
# File 'lib/jets/builders/code_builder.rb', line 412

def cache_check_message
  if File.exist?("#{Jets.build_root}/cache")
    puts "The #{Jets.build_root}/cache folder exists. Incrementally re-building the jets using the cache.  To clear the cache: rm -rf #{Jets.build_root}/cache"
  end
end

#check_ruby_versionObject



418
419
420
421
422
423
424
# File 'lib/jets/builders/code_builder.rb', line 418

def check_ruby_version
  unless ruby_version_supported?
    puts "You are using ruby version #{RUBY_VERSION} which is not supported by Jets."
    ruby_variant = Jets::RUBY_VERSION.split('.')[0..1].join('.') + '.x'
    abort("Jets uses ruby #{Jets::RUBY_VERSION}.  You should use a variant of ruby #{ruby_variant}".colorize(:red))
  end
end

#clean_old_submodulesObject

When using submodules, bundler leaves old submodules behind. Over time this inflates the size of the the bundled gems. So we’ll clean it up.



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
# File 'lib/jets/builders/code_builder.rb', line 343

def clean_old_submodules
  lockfile = "#{cache_area}/Gemfile.lock"
  # https://stackoverflow.com/questions/38800129/parsing-a-gemfile-lock-with-bundler
  parser = Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile))
  specs = parser.specs

  # specs = Bundler.load.specs
  # IE: spec.source.to_s: "https://github.com/tongueroo/webpacker.git (at jets@a8c4661)"
  submoduled_specs = specs.select do |spec|
    spec.source.to_s =~ /@\w+\)/
  end

  # find git shas to keep
  # IE: ["a8c4661", "abc4661"]
  git_shas = submoduled_specs.map do |spec|
    md = spec.source.to_s.match(/@(\w+)\)/)
    git_sha = md[1]
  end
  puts "git_shas #{git_shas.inspect}"

  # IE: /tmp/jets/demo/cache/bundled/gems/ruby/2.5.0/bundler/gems/webpacker-a8c46614c675
  Dir.glob("#{cache_area}/bundled/gems/ruby/2.5.0/bundler/gems/*").each do |path|
    sha = path.split('-').last[0..6] # only first 7 chars of the git sha
    unless git_shas.include?(sha)
      puts "Removing old submoduled gem: #{path}"
      FileUtils.rm_rf(path) # REMOVE old submodule directory
    end
  end
end

#clean_startObject

Cleans out non-cached files like code-*.zip in Jets.build_root for a clean start. Also ensure that the /tmp/jets/project build root exists.

Most files are kept around after the build process for inspection and debugging. So we have to clean out the files. But we only want to clean out some of the files.



148
149
150
151
# File 'lib/jets/builders/code_builder.rb', line 148

def clean_start
  Dir.glob("#{Jets.build_root}/code/code-*.zip").each { |f| FileUtils.rm_f(f) }
  FileUtils.mkdir_p(Jets.build_root) # /tmp/jets/demo
end

#compile_assetsObject

This happens in the current app directory not the tmp app_root for simplicity



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/jets/builders/code_builder.rb', line 128

def compile_assets
  headline "Compling assets in current project directory"
  # Thanks: https://stackoverflow.com/questions/4195735/get-list-of-gems-being-used-by-a-bundler-project
  webpacker_loaded = Gem.loaded_specs.keys.include?("webpacker")
  return unless webpacker_loaded

  sh("yarn install")
  webpack_bin = File.exist?("#{Jets.root}bin/webpack") ?
      "bin/webpack" :
      `which webpack`.strip
  sh("JETS_ENV=#{Jets.env} #{webpack_bin}")
end

#copy_bundled_to_app_rootObject



226
227
228
229
230
231
232
233
234
# File 'lib/jets/builders/code_builder.rb', line 226

def copy_bundled_to_app_root
  app_root_bundled = "#{full(tmp_app_root)}/bundled"
  if File.exist?(app_root_bundled)
    puts "Removing current bundled from project"
    FileUtils.rm_rf(app_root_bundled)
  end
  # Leave #{Jets.build_root}/bundled behind to act as cache
  FileUtils.cp_r("#{cache_area}/bundled", app_root_bundled)
end

#copy_gemfilesObject



373
374
375
376
377
# File 'lib/jets/builders/code_builder.rb', line 373

def copy_gemfiles
  FileUtils.mkdir_p(cache_area)
  FileUtils.cp("#{@full_project_path}Gemfile", "#{cache_area}/Gemfile")
  FileUtils.cp("#{@full_project_path}Gemfile.lock", "#{cache_area}/Gemfile.lock")
end

#copy_projectObject

Copy project into temporary directory. Do this so we can keep the project directory untouched and we can also remove a bunch of unnecessary files like logs before zipping it up.



156
157
158
159
160
161
162
163
164
165
# File 'lib/jets/builders/code_builder.rb', line 156

def copy_project
  headline "Copying current project directory to temporary build area: #{full(tmp_app_root)}"
  FileUtils.rm_rf(full(tmp_app_root)) # remove current app_root folder
  move_node_modules(Jets.root, Jets.build_root)
  begin
    FileUtils.cp_r(@full_project_path, full(tmp_app_root))
  ensure
    move_node_modules(Jets.build_root, Jets.root) # move node_modules directory back
  end
end

#create_zip_file(fake = nil) ⇒ Object



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
# File 'lib/jets/builders/code_builder.rb', line 264

def create_zip_file(fake=nil)
  puts "Creating zip file."
  temp_code_zipfile = "#{Jets.build_root}/code/code-temp.zip"
  FileUtils.mkdir_p(File.dirname(temp_code_zipfile))

  # Use fake if testing CloudFormation only
  if fake
    hello_world = "/tmp/hello.js"
    puts "Uploading tiny #{hello_world} file to S3 for quick testing.".colorize(:red)
    code = IO.read(File.expand_path("../node-hello.js", __FILE__))
    IO.write(hello_world, code)
    command = "zip --symlinks -rq #{temp_code_zipfile} #{hello_world}"
  else
    # https://serverfault.com/questions/265675/how-can-i-zip-compress-a-symlink
    command = "cd #{full(tmp_app_root)} && zip --symlinks -rq #{temp_code_zipfile} ."
  end

  sh(command)

  # we can get the md5 only after the file has been created
  md5 = Digest::MD5.file(temp_code_zipfile).to_s[0..7]
  md5_zip_dest = "#{Jets.build_root}/code/code-#{md5}.zip"
  FileUtils.mkdir_p(File.dirname(md5_zip_dest))
  FileUtils.mv(temp_code_zipfile, md5_zip_dest)
  # mv /tmp/jets/demo/code/code-temp.zip /tmp/jets/demo/code/code-a8a604aa.zip

  file_size = number_to_human_size(File.size(md5_zip_dest))
  puts "Zip file with code and bundled linux ruby created at: #{md5_zip_dest.colorize(:green)} (#{file_size})"

  # Save state
  IO.write("#{Jets.build_root}/code/current-md5-filename.txt", md5_zip_dest)
  # Much later: ship, base_child_builder need set an s3_key which requires
  # the md5_zip_dest.
  # It is a pain to pass this all the way up from the
  # CodeBuilder class.
  # Let's store the "/tmp/jets/demo/code/code-a8a604aa.zip" into a
  # file that can be read from any places where this is needed.
  # Can also just generate a "fake file" for specs
end

#ensure_build_cache_bundle_config_exists!Object

On circleci the “#Jets.build_root/.bundle/config” doesnt exist this only happens with ssh debugging, not when the ci.sh script gets ran. But on macosx it exists. Dont know why this is the case.



253
254
255
256
257
258
259
260
261
262
# File 'lib/jets/builders/code_builder.rb', line 253

def ensure_build_cache_bundle_config_exists!
  text ="---\nBUNDLE_PATH: \"bundled/gems\"\nBUNDLE_WITHOUT: \"development:test\"\n"
  bundle_config = "#{cache_area}/.bundle/config"
  FileUtils.mkdir_p(File.dirname(bundle_config))
  IO.write(bundle_config, text)
end

#excludesObject



379
380
381
382
383
384
385
386
387
388
389
# File 'lib/jets/builders/code_builder.rb', line 379

def excludes
  excludes = %w[.git tmp log spec]
  excludes += get_excludes("#{full(tmp_app_root)}/.gitignore")
  excludes += get_excludes("#{full(tmp_app_root)}/.dockerignore")
  excludes = excludes.reject do |p|
    jetskeep.find do |keep|
      p.include?(keep)
    end
  end
  excludes
end

#extract_gemsObject



122
123
124
125
# File 'lib/jets/builders/code_builder.rb', line 122

def extract_gems
  headline "Replacing compiled gems with AWS Lambda Linux compiled versions."
  GemReplacer.new(Jets::RUBY_VERSION, lambdagem_options).run
end

#extract_rubyObject



117
118
119
120
# File 'lib/jets/builders/code_builder.rb', line 117

def extract_ruby
  headline "Setting up a vendored copy of ruby."
  Lambdagem::Extract::Ruby.new(Jets::RUBY_VERSION, lambdagem_options).run
end

#finish_app_root_setupObject



99
100
101
102
103
104
105
106
# File 'lib/jets/builders/code_builder.rb', line 99

def finish_app_root_setup
  return if poly_only?

  copy_bundled_to_app_root
  setup_bundle_config
  extract_ruby
  extract_gems
end

#full(relative_path) ⇒ Object

Provide pretty clear way to desinate full path. full(“bundled”) => /tmp/jets/demo/bundled



442
443
444
# File 'lib/jets/builders/code_builder.rb', line 442

def full(relative_path)
  "#{Jets.build_root}/#{relative_path}"
end

#generate_node_shimsObject



192
193
194
195
196
197
198
199
200
201
# File 'lib/jets/builders/code_builder.rb', line 192

def generate_node_shims
  headline "Generating node shims in the handlers folder."
  # Crucial that the Dir.pwd is in the tmp_app_root because for
  # Jets::Builders::app_files because Jets.boot set ups
  # autoload_paths and this is how project classes are loaded.
  Jets::Commands::Build.app_files.each do |path|
    handler = Jets::Builders::HandlerGenerator.new(path)
    handler.generate
  end
end

#get_excludes(file) ⇒ Object



391
392
393
394
395
396
397
398
# File 'lib/jets/builders/code_builder.rb', line 391

def get_excludes(file)
  path = file
  return [] unless File.exist?(path)

  exclude = File.read(path).split("\n")
  exclude.map {|i| i.strip}.reject {|i| i =~ /^#/ || i.empty?}
  # IE: ["/handlers", "/bundled*", "/vendor/jets]
end

#headline(message) ⇒ Object



462
463
464
# File 'lib/jets/builders/code_builder.rb', line 462

def headline(message)
  puts "=> #{message}".colorize(:cyan)
end

#jetskeepObject

We clean out ignored files pretty aggressively. So provide a way for users to keep files from being cleaned ou.



402
403
404
405
406
407
408
409
410
# File 'lib/jets/builders/code_builder.rb', line 402

def jetskeep
  defaults = %w[pack handlers]
  path = Jets.root + ".jetskeep"
  return defaults unless path.exist?

  keep = path.read.split("\n")
  keep = keep.map {|i| i.strip}.reject {|i| i =~ /^#/ || i.empty?}
  (defaults + keep).uniq
end

#lambdagem_optionsObject



109
110
111
112
113
114
115
# File 'lib/jets/builders/code_builder.rb', line 109

def lambdagem_options
  {
    s3: "lambdagems",
    build_root: cache_area, # used in lambdagem
    project_root: full(tmp_app_root), # used in gem_replacer and lambdagem
  }
end

#move_node_modules(source_folder, dest_folder) ⇒ Object

Move the node modules to the tmp build folder to speed up project copying. A little bit risky because a ctrl-c in the middle of the project copying results in a missing node_modules but user can easily rebuild that.

Tesing shows 6.623413 vs 0.027754 speed improvement.



173
174
175
176
177
178
179
# File 'lib/jets/builders/code_builder.rb', line 173

def move_node_modules(source_folder, dest_folder)
  source = "#{source_folder}/node_modules"
  dest = "#{dest_folder}/node_modules"
  if File.exist?(source)
    FileUtils.mv(source, dest)
  end
end

#poly_only?Boolean

Finds out of the app has polymorphic functions only and zero ruby functions. In this case, we can skip a lot of the ruby related building and speed up the deploy process.

Returns:

  • (Boolean)


86
87
88
89
# File 'lib/jets/builders/code_builder.rb', line 86

def poly_only?
  return true if ENV['POLY_ONLY'] # bypass to allow rapid development of handlers
  Jets::Commands::Build.poly_only?
end

#reconfigure_development_webpackerObject

Bit hacky but this saves the user from accidentally forgetting to change this when they deploy a jets project in development mode



205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/jets/builders/code_builder.rb', line 205

def reconfigure_development_webpacker
  return unless Jets.env.development?
  headline "Reconfiguring webpacker development settings for AWS Lambda."

  webpacker_yml = "#{full(tmp_app_root)}/config/webpacker.yml"
  return unless File.exist?(webpacker_yml)

  config = YAML.load_file(webpacker_yml)
  config["development"]["compile"] = false # force this to be false for deployment
  new_yaml = YAML.dump(config)
  IO.write(webpacker_yml, new_yaml)
end

#reconfigure_ruby_versionObject

This is in case the user has a 2.5.x variant. Force usage of ruby version that jets supports The lambda server only has ruby 2.5.0 installed.



221
222
223
224
# File 'lib/jets/builders/code_builder.rb', line 221

def reconfigure_ruby_version
  ruby_version = "#{full(tmp_app_root)}/.ruby-version"
  IO.write(ruby_version, Jets::RUBY_VERSION)
end

#ruby_version_supported?Boolean

Returns:

  • (Boolean)


426
427
428
429
430
431
432
433
434
# File 'lib/jets/builders/code_builder.rb', line 426

def ruby_version_supported?
  pattern = /(\d+)\.(\d+)\.(\d+)/
  md = RUBY_VERSION.match(pattern)
  ruby = {major: md[1], minor: md[2]}
  md = Jets::RUBY_VERSION.match(pattern)
  jets = {major: md[1], minor: md[2]}

  ruby[:major] == jets[:major] && ruby[:minor] == jets[:minor]
end

#setup_bundle_configObject



236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/jets/builders/code_builder.rb', line 236

def setup_bundle_config
  ensure_build_cache_bundle_config_exists!

  # Override project's .bundle/config and ensure that .bundle/config matches
  # at these 2 spots:
  #   app_root/.bundle/config
  #   bundled/gems/.bundle/config
  cache_bundle_config = "#{cache_area}/.bundle/config"
  app_bundle_config = "#{full(tmp_app_root)}/.bundle/config"
  FileUtils.mkdir_p(File.dirname(app_bundle_config))
  FileUtils.cp(cache_bundle_config, app_bundle_config)
end

#sh(command) ⇒ Object



455
456
457
458
459
460
# File 'lib/jets/builders/code_builder.rb', line 455

def sh(command)
  puts "=> #{command}".colorize(:green)
  success = system(command)
  abort("#{command} failed to run") unless success
  success
end

#start_app_root_setupObject



91
92
93
94
95
96
# File 'lib/jets/builders/code_builder.rb', line 91

def start_app_root_setup
  tidy_project
  reconfigure_development_webpacker
  reconfigure_ruby_version
  generate_node_shims
end

#tidy_projectObject

Because we’re removing files (something dangerous) use full paths.



182
183
184
185
186
187
188
189
190
# File 'lib/jets/builders/code_builder.rb', line 182

def tidy_project
  headline "Tidying project: removing ignored files to reduce package size."
  excludes.each do |exclude|
    exclude = exclude.sub(%r{^/},'') # remove leading slash
    remove_path = "#{full(tmp_app_root)}/#{exclude}"
    FileUtils.rm_rf(remove_path)
    # puts "  rm -rf #{remove_path}" # uncomment to debug
  end
end

#tmp_app_rootObject



451
452
453
# File 'lib/jets/builders/code_builder.rb', line 451

def tmp_app_root
  self.class.tmp_app_root
end