Class: Cinnabar::GemPath

Inherits:
Object
  • Object
show all
Defined in:
lib/cinnabar/gem_path.rb

Overview

Note:

Prefer using the cache whenever possible; RubyGems operations can be slow.

Cached resolver for gem load paths.

GemPathCore performs expensive operations (loading RubyGems, resolving specs, installing missing gems). This class adds a JSON cache layer to reuse results and avoid repeated slow calls.

What it caches

  • A Hash mapping gem_name => [full_require_paths...]
  • Serialized to JSON on disk (see #cache_file)

Typical usage

  1. Create an instance with a set of gems.
  2. Call #append_load_path! to add those directories to $LOAD_PATH.

Constant Summary collapse

CoreMod =

Alias to the low-level implementation module.

Returns:

  • (Module)
Cinnabar::GemPathCore
DEFAULT_OPTS =

Default options for initialization.

Returns:

  • (Hash)
{
  cache_dir: File.expand_path('~/.cache/ruby'),
  cache_file: 'gem_path.json',
  gems: [],
  install_gem: true,
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Cinnabar::GemPath

Creates a cache-backed gem path resolver.

Behavior

  • Normalizes gems to unique, non-empty strings.
  • If cache file exists, it is decoded and then refreshed:
    • Missing gems are generated and merged into the cache.
    • Broken entries (paths not existing) are regenerated.
  • If no cache exists, it generates paths for all gems and writes the cache.

Examples:

Create and append load paths


gems = %w[rdoc logger irb reline fiddle]

{ gems: }
  .then { Cinnabar::GemPath.new _1 }
  .append_load_path!

Parameters:

  • opts (Hash) (defaults to: {})

    options overriding DEFAULT_OPTS

Options Hash (opts):

  • :cache_dir (String)
  • :cache_file (String)
  • :gems (Array<String,Symbol>)
  • :install_gem (Boolean)


233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/cinnabar/gem_path.rb', line 233

def initialize(opts = {})
  options = DEFAULT_OPTS.merge(opts || {})

  gems, cache_dir, cache_file, @install_gem =
    options.values_at(:gems, :cache_dir, :cache_file, :install_gem)

  @gems = Array(gems).map(&:to_s).reject(&:empty?).uniq

  raise 'Empty @gems!' if @gems.empty?

  @cache_file = init_cache_file(cache_dir, cache_file)
  @cache_hash = {}

  if @cache_file.exist?
    decode_cache_file
    return
  end

  merge_generated_paths!(@gems)
end

Instance Attribute Details

#cache_filePathname

Returns cache file path (*.json).

Returns:

  • (Pathname)

    cache file path (*.json)



187
188
189
# File 'lib/cinnabar/gem_path.rb', line 187

def cache_file
  @cache_file
end

#cache_hashHash{String=>Array<String>} (readonly)

The in-memory cache hash.

Returns:

  • (Hash{String=>Array<String>})

    gem name => full require paths



192
193
194
# File 'lib/cinnabar/gem_path.rb', line 192

def cache_hash
  @cache_hash
end

#gemsArray<String>

Returns normalized gem names (unique, non-empty).

Returns:

  • (Array<String>)

    normalized gem names (unique, non-empty)



187
# File 'lib/cinnabar/gem_path.rb', line 187

attr_accessor :cache_file, :gems, :install_gem

#install_gemBoolean

Returns whether missing gems should be installed automatically.

Returns:

  • (Boolean)

    whether missing gems should be installed automatically



187
# File 'lib/cinnabar/gem_path.rb', line 187

attr_accessor :cache_file, :gems, :install_gem

Instance Method Details

#append_load_path!void

This method returns an undefined value.

Appends cached gem paths into $LOAD_PATH.

This is typically called after initialization. It will add each directory in the cache for the configured gems to Ruby's load path.

Notes

  • This is idempotent: it avoids inserting duplicates.
  • It mutates the global $LOAD_PATH ($:).


277
278
279
280
281
282
283
284
285
286
# File 'lib/cinnabar/gem_path.rb', line 277

def append_load_path!
  @cache_hash
    .filter { |k, _| @gems.include? k.to_s }
    .each_value do |vals|
      Array(vals).each do |dir|
        # $: is $LOAD_PATH
        $:.push(dir) unless $:.include?(dir) # rubocop:disable Style/SpecialGlobalVars
      end
    end
end

#atomic_write_cache_file(content) ⇒ void (protected)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Writes cache content to disk atomically.

It writes to a temporary file and renames it to the final cache path. This reduces the chance of leaving a partially-written JSON file when the process crashes or is interrupted mid-write.

Parameters:

  • content (String)

    serialized JSON content



300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/cinnabar/gem_path.rb', line 300

def atomic_write_cache_file(content)
  path = @cache_file.to_s
  tmp = "#{path}.tmp.#{$$}" # rubocop:disable Style/SpecialGlobalVars
  begin
    File.write(tmp, content)
    File.rename(tmp, path)
  ensure
    begin
      File.unlink(tmp)
    rescue StandardError
      nil
    end
  end
end

#decode_cache_filevoid (protected)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Decodes the cache file and refreshes any missing/broken entries.

  • Loads #cache_hash from disk.
  • Generates entries for missing gems in #gems.
  • Regenerates entries whose paths no longer exist on disk.


365
366
367
368
369
# File 'lib/cinnabar/gem_path.rb', line 365

def decode_cache_file
  @cache_hash = try_decode_cache_hash
  refresh_missing_gems!
  refresh_broken_gems!
end

#init_cache_file(dir, file) ⇒ Pathname (protected)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Resolves the cache file path.

  • If file is absolute, it is used directly.
  • Otherwise, it is resolved relative to dir.
  • Ensures the directory exists (mkpath).

Parameters:

  • dir (String, Pathname)

    base directory

  • file (String, Pathname)

    file name or absolute path

Returns:

  • (Pathname)

    resolved cache file path



326
327
328
329
330
331
332
333
334
335
# File 'lib/cinnabar/gem_path.rb', line 326

def init_cache_file(dir, file)
  f = Pathname(file)

  if f.absolute?
    f
  else
    Pathname(dir).join(f)
  end
    .tap { _1.dirname.mkpath }
end

#try_decode_cache_hashHash (protected)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Note:

This method intentionally rescues Exception per the original design.

Attempts to decode the cache JSON file into a Hash.

If decoding fails for any reason, it warns and removes the cache file, then returns an empty hash.

Returns:

  • (Hash)

    decoded hash (or {} if decode fails)



346
347
348
349
350
351
352
353
354
# File 'lib/cinnabar/gem_path.rb', line 346

def try_decode_cache_hash
  @cache_file
    .read
    .then { JSON.parse _1 }
rescue Exception => e # rubocop:disable Lint/RescueException
  Kernel.warn "[WARN] Failed to decode json file; error: #{e}; unlink #{@cache_file}"
  @cache_file.unlink
  {}
end

#update_cache_filevoid

This method returns an undefined value.

Writes the in-memory cache to disk.

This serializes #cache_hash as JSON and persists it using an atomic write strategy (see #atomic_write_cache_file).



260
261
262
263
264
# File 'lib/cinnabar/gem_path.rb', line 260

def update_cache_file
  @cache_hash
    .then { JSON.dump _1 }
    .then { atomic_write_cache_file _1 }
end