Class: Bundler::Spinel::Vendorer

Inherits:
Object
  • Object
show all
Defined in:
lib/bundler/spinel/vendorer.rb

Overview

“Make it work” — the plugin’s primary job. Spinel has no load path (plain ‘require “x”` resolves only against <spinel>/lib) and inlines `require_relative`. So to actually use a resolved dependency in a Spinel build, its source has to be placed somewhere Spinel will follow, with the require wiring generated. This is the reusable form of what projects do by hand today (e.g. Toy’s build_tep_app.sh concatenation, Roundhouse vendoring part of Tep).

Given a Gemfile.lock, vendor each gem’s ‘lib/` into `<into>/<name>/` and emit `<into>/deps.rb` — a manifest of `require_relative`s in lock order. A Spinel program then just does `require_relative “vendor/spinel/deps”`.

Gating is layered on but advisory here: placement and compatibility are different jobs. ‘vendor` warns on non-compatible gems (so the experience is nicer) but still places them; `check` is the hard gate.

Instance Method Summary collapse

Constructor Details

#initialize(engine: Engine.new, ledger: Ledger.new) ⇒ Vendorer

Returns a new instance of Vendorer.



25
26
27
28
29
# File 'lib/bundler/spinel/vendorer.rb', line 25

def initialize(engine: Engine.new, ledger: Ledger.new)
  @engine = engine
  @ledger = ledger
  @fetcher = GemFetcher.new
end

Instance Method Details

#resolve_source(spec, lock_dir) ⇒ Object

path:/git: lockfile sources (toy ↔ tep is the headline case) point at a local tree; we don’t go through ‘gem fetch`. For GEM sources we fall back to the cache-backed RubyGems fetcher. Issue: OriPekelman/spinelgems#3.



92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/bundler/spinel/vendorer.rb', line 92

def resolve_source(spec, lock_dir)
  src = spec.source
  if src.respond_to?(:path) && src.path
    path = src.path.to_s
    # Bundler stores PATH as relative-to-lockfile; resolve to abs.
    path = File.expand_path(path, lock_dir) unless File.absolute_path?(path)
    unless File.directory?(path)
      raise Error, "path: source for #{spec.name} not found: #{path}"
    end
    return path
  end
  @fetcher.fetch(spec.name, spec.version.to_s)
end

#topo_sort(specs) ⇒ Object

Order specs so every gem’s runtime dependencies come before it — a DFS post-order over ‘spec.dependencies`, with an alphabetical tiebreak for determinism and a visiting-set guard so a dependency cycle degrades to some stable order instead of looping. Deps not present in this lockset (stdlib/default gems) are skipped. (spinelgems#19)



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/bundler/spinel/vendorer.rb', line 69

def topo_sort(specs)
  by_name = specs.each_with_object({}) { |s, h| h[s.name] = s }
  ordered = []
  state = {} # name => :visiting | :done
  visit = lambda do |spec|
    st = state[spec.name]
    return if st == :done || st == :visiting
    state[spec.name] = :visiting
    spec.dependencies.sort_by(&:name).each do |dep|
      dn = dep.respond_to?(:name) ? dep.name : dep.to_s
      visit.call(by_name[dn]) if by_name[dn]
    end
    state[spec.name] = :done
    ordered << spec
  end
  specs.sort_by(&:name).each { |s| visit.call(s) }
  ordered
end

#vendor(lockfile = "Gemfile.lock", into: "vendor/spinel", warn_incompatible: true, ext_overrides: {}, ext_disable: []) ⇒ Object



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
# File 'lib/bundler/spinel/vendorer.rb', line 31

def vendor(lockfile = "Gemfile.lock", into: "vendor/spinel", warn_incompatible: true,
           ext_overrides: {}, ext_disable: [])
  parsed = Bundler::LockfileParser.new(File.read(lockfile))
  lock_dir = File.dirname(File.expand_path(lockfile))
  into = File.expand_path(into)
  FileUtils.mkdir_p(into)
  disable = (ext_disable + ENV["SPINEL_EXT_DISABLE"].to_s.split(",")).map(&:strip).reject(&:empty?)

  manifest = []
  exts = 0
  # Topological order (dependencies before dependents), not the lockfile's
  # alphabetical `specs` (spinelgems#19): Spinel has no load path, so
  # deps.rb is a *flattened single load* — each gem's entrypoint
  # require_relative'd once, in order. A dependent loaded before its
  # dependency would reference not-yet-defined constants. tep→spinel_kit
  # is the first real case (it sorted right only by alphabetical luck).
  topo_sort(parsed.specs).each do |spec|
    name = spec.name
    version = spec.version.to_s
    src = resolve_source(spec, lock_dir)
    dest = File.join(into, name)
    place(src, dest)
    exts += wire_extensions(src, dest, ext_overrides, disable)
    if (target = require_target(name, dest))
      manifest << { require: target, libdir: "#{File.basename(dest)}/lib" }
    end
    note_compat(name, version) if warn_incompatible
  end

  write_manifest(into, manifest)
  { into: into, count: manifest.size, extensions: exts }
end