class Bootsnap::CLI
def cache_dir=(dir)
def cache_dir=(dir) @cache_dir = File.expand_path(File.join(dir, "bootsnap/compile-cache")) end
def exclude_pattern(pattern)
def exclude_pattern(pattern) (@exclude_patterns ||= []) << Regexp.new(pattern) self.exclude = Regexp.union(@exclude_patterns) end
def fix_default_encoding
def fix_default_encoding if Encoding.default_external == Encoding::US_ASCII Encoding.default_external = Encoding::UTF_8 begin yield ensure Encoding.default_external = Encoding::US_ASCII end else yield end end
def initialize(argv)
def initialize(argv) @argv = argv self.cache_dir = ENV.fetch("BOOTSNAP_CACHE_DIR", "tmp/cache") self.compile_gemfile = false self.exclude = nil self.verbose = false self.jobs = Etc.nprocessors self.iseq = true self.yaml = true self.json = true end
def invalid_usage!(message)
def invalid_usage!(message) STDERR.puts message STDERR.puts STDERR.puts parser 1 end
def list_files(path, pattern)
def list_files(path, pattern) if File.directory?(path) Dir[File.join(path, pattern), sort: false] elsif File.exist?(path) [path] else [] end end
def list_files(path, pattern)
def list_files(path, pattern) if File.directory?(path) Dir[File.join(path, pattern)] elsif File.exist?(path) [path] else [] end end
def parser
def parser @parser ||= OptionParser.new do |opts| opts.banner = "Usage: bootsnap COMMAND [ARGS]" opts.separator "" opts.separator "GLOBAL OPTIONS" opts.separator "" help = <<~HELP Path to the bootsnap cache directory. Defaults to tmp/cache HELP opts.on("--cache-dir DIR", help.strip) do |dir| self.cache_dir = dir end help = <<~HELP Print precompiled paths. HELP opts.on("--verbose", "-v", help.strip) do self.verbose = true end help = <<~HELP Number of workers to use. Default to number of processors, set to 0 to disable multi-processing. HELP opts.on("--jobs JOBS", "-j", help.strip) do |jobs| self.jobs = Integer(jobs) end opts.separator "" opts.separator "COMMANDS" opts.separator "" opts.separator " precompile [DIRECTORIES...]: Precompile all .rb files in the passed directories" help = <<~HELP Precompile the gems in Gemfile HELP opts.on("--gemfile", help) { self.compile_gemfile = true } help = <<~HELP Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api' HELP opts.on("--exclude PATTERN", help) { |pattern| exclude_pattern(pattern) } help = <<~HELP Disable ISeq (.rb) precompilation. HELP opts.on("--no-iseq", help) { self.iseq = false } help = <<~HELP Disable YAML precompilation. HELP opts.on("--no-yaml", help) { self.yaml = false } help = <<~HELP Disable JSON precompilation. HELP opts.on("--no-json", help) { self.json = false } end end
def precompile_command(*sources)
def precompile_command(*sources) require "bootsnap/compile_cache/iseq" require "bootsnap/compile_cache/yaml" require "bootsnap/compile_cache/json" fix_default_encoding do Bootsnap::CompileCache::ISeq.cache_dir = cache_dir Bootsnap::CompileCache::YAML.init! Bootsnap::CompileCache::YAML.cache_dir = cache_dir Bootsnap::CompileCache::JSON.init! Bootsnap::CompileCache::JSON.cache_dir = cache_dir @work_pool = WorkerPool.create(size: jobs, jobs: { ruby: method(:precompile_ruby), yaml: method(:precompile_yaml), json: method(:precompile_json), }) @work_pool.spawn main_sources = sources.map { |d| File.expand_path(d) } precompile_ruby_files(main_sources) precompile_yaml_files(main_sources) precompile_json_files(main_sources) if compile_gemfile # Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling. gem_exclude = Regexp.union([exclude, "/spec/", "/test/"].compact) precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude) # Gems that include JSON or YAML files usually don't put them in `lib/`. # So we look at the gem root. gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems\/[^/]+} gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq precompile_yaml_files(gem_paths, exclude: gem_exclude) precompile_json_files(gem_paths, exclude: gem_exclude) end if (exitstatus = @work_pool.shutdown) exit(exitstatus) end end 0 end
def precompile_json(*json_files)
def precompile_json(*json_files) Array(json_files).each do |json_file| if CompileCache::JSON.precompile(json_file) STDERR.puts(json_file) if verbose end end end
def precompile_json_files(load_paths, exclude: self.exclude)
def precompile_json_files(load_paths, exclude: self.exclude) return unless json load_paths.each do |path| if !exclude || !exclude.match?(path) list_files(path, "**/*.json").each do |json_file| # We ignore hidden files to not match the various .config.json files if !File.basename(json_file).start_with?(".") && (!exclude || !exclude.match?(json_file)) @work_pool.push(:json, json_file) end end end end end
def precompile_ruby(*ruby_files)
def precompile_ruby(*ruby_files) Array(ruby_files).each do |ruby_file| if CompileCache::ISeq.precompile(ruby_file) STDERR.puts(ruby_file) if verbose end end end
def precompile_ruby_files(load_paths, exclude: self.exclude)
def precompile_ruby_files(load_paths, exclude: self.exclude) return unless iseq load_paths.each do |path| if !exclude || !exclude.match?(path) list_files(path, "**/{*.rb,*.rake,Rakefile}").each do |ruby_file| if !exclude || !exclude.match?(ruby_file) @work_pool.push(:ruby, ruby_file) end end end end end
def precompile_yaml(*yaml_files)
def precompile_yaml(*yaml_files) Array(yaml_files).each do |yaml_file| if CompileCache::YAML.precompile(yaml_file) STDERR.puts(yaml_file) if verbose end end end
def precompile_yaml_files(load_paths, exclude: self.exclude)
def precompile_yaml_files(load_paths, exclude: self.exclude) return unless yaml load_paths.each do |path| if !exclude || !exclude.match?(path) list_files(path, "**/*.{yml,yaml}").each do |yaml_file| # We ignore hidden files to not match the various .ci.yml files if !File.basename(yaml_file).start_with?(".") && (!exclude || !exclude.match?(yaml_file)) @work_pool.push(:yaml, yaml_file) end end end end end
def run
def run parser.parse!(argv) command = argv.shift method = "#{command}_command" if respond_to?(method) public_send(method, *argv) else invalid_usage!("Unknown command: #{command}") end end