Module: Vcdeps::Vendor

Defined in:
lib/vcdeps/vendor.rb

Overview

Syncs an Installed's runtime DLLs, per-port copyright files, and a standalone Fiddle-preload shim into a vendor directory (§2.2/§2.7). This is a SYNC, not a copy: vendor! OWNS *.dll, \licenses* and \preload.rb — it overwrites the computed write list and DELETES any owned file no longer on it (so a port removed from vcpkg.json leaves no stale DLL the shim would preload and the precompiled-gem glob would ship). Nothing else under into is touched, so authors may keep unrelated assets there.

Constant Summary collapse

SHIM_NAME =
"preload.rb"
LICENSES_DIR =
"licenses"

Class Method Summary collapse

Class Method Details

.shim_sourceObject

The generated preload shim (exact §2.7 template). Standalone: stdlib only, no vcdeps require, safe to require twice, safe with zero DLLs present.



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
# File 'lib/vcdeps/vendor.rb', line 83

def shim_source
  <<~SHIM
    # frozen_string_literal: true
    #
    # Generated by vcdeps #{Vcdeps::VERSION}. Do not edit; regenerate with `vcdeps vendor`.
    #
    # Preloads the vendored vcpkg runtime DLLs by ABSOLUTE path before the C
    # extension loads. Windows resolves an extension's dependent DLLs via the
    # standard search order, which checks the loaded-module list (step 4) long
    # before PATH (step 12) and NEVER checks the .so's own directory — so loading
    # each DLL here, by full path, is what makes `require "<gem>/<gem>"` work.
    #
    # The dlopen handles are intentionally leaked for process lifetime: the DLLs
    # must stay loaded as long as the extension is loaded.
    require "fiddle"

    pending = Dir[File.join(__dir__, "*.dll")].sort
    until pending.empty?
      progressed = false
      pending.delete_if do |dll|
        Fiddle.dlopen(dll)
        progressed = true
        true
      rescue Fiddle::DLError
        false   # depends on a sibling not yet loaded — retry next pass
      end
      break unless progressed   # truly unloadable; let the require raise its own LoadError
    end
  SHIM
end

.sweep_stale!(into, kept_dll_names, kept_license_names, write_shim) ⇒ Object

Delete any *.dll directly in into not on the kept list, any licenses* file not kept, and a stale shim when no shim is being written.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/vcdeps/vendor.rb', line 118

def sweep_stale!(into, kept_dll_names, kept_license_names, write_shim)
  Dir[File.join(into.tr("\\", "/"), "*.dll")].each do |dll|
    File.delete(dll) unless kept_dll_names.include?(File.basename(dll).downcase)
  end

  ldir = File.join(into.tr("\\", "/"), LICENSES_DIR)
  if File.directory?(ldir)
    Dir[File.join(ldir, "*")].each do |f|
      next unless File.file?(f)

      File.delete(f) unless kept_license_names.include?(File.basename(f).downcase)
    end
    # Remove an empty licenses dir we no longer populate.
    begin
      Dir.rmdir(ldir) if Dir.empty?(ldir)
    rescue SystemCallError
      nil
    end
  end

  shim = File.join(into.tr("\\", "/"), SHIM_NAME)
  File.delete(shim) if !write_shim && File.exist?(shim)
end

.sync!(installed, into:, licenses: true, shim: true, out: $stderr) ⇒ Object

Returns the list of files written (absolute, backslashed). See §2.2 for the four-case table; stale owned files are deleted whenever into exists.



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
# File 'lib/vcdeps/vendor.rb', line 23

def sync!(installed, into:, licenses: true, shim: true, out: $stderr)
  dlls = installed.dlls
  copyrights = licenses ? installed.copyright_files : {}

  # The write plan: a DLL is vendored only when there is something to vendor;
  # the shim is written only when DLLs exist; licenses ship whenever copyright
  # files exist (including static-md, where there are no DLLs).
  write_dlls   = dlls
  write_shim   = shim && !dlls.empty?
  write_copies = copyrights

  # Nothing to write AND nothing to clean (into absent) -> [] without
  # creating `into` (§2.2 last case).
  if write_dlls.empty? && write_copies.empty? && !write_shim &&
     !File.directory?(into)
    return []
  end

  written = []
  FileUtils.mkdir_p(into)

  # Owned DLLs we intend to keep (base names), for the stale sweep.
  kept_dll_names = []
  write_dlls.each do |src|
    base = File.basename(src)
    dest = File.join(into, base)
    FileUtils.cp(src, dest)
    kept_dll_names << base.downcase
    written << win(dest)
    warn_on_ruby_bin_collision(base, out)
  end

  # Licenses.
  kept_license_names = []
  unless write_copies.empty?
    ldir = File.join(into, LICENSES_DIR)
    FileUtils.mkdir_p(ldir)
    write_copies.each do |port, src|
      dest = File.join(ldir, "#{port}-copyright.txt")
      FileUtils.cp(src, dest)
      kept_license_names << File.basename(dest).downcase
      written << win(dest)
    end
  end

  # Shim.
  shim_path = File.join(into, SHIM_NAME)
  if write_shim
    File.write(shim_path, shim_source)
    written << win(shim_path)
  end

  # Stale sweep of OWNED files only.
  sweep_stale!(into, kept_dll_names, kept_license_names, write_shim)

  written
end

.warn_on_ruby_bin_collision(base, out) ⇒ Object

Warn when a vendored DLL base name also exists in Ruby's bindir (zlib1.dll/ffi-8.dll/yaml.dll shadowing, R§9.2) — first-loaded wins.



144
145
146
147
148
149
150
151
152
153
154
# File 'lib/vcdeps/vendor.rb', line 144

def warn_on_ruby_bin_collision(base, out)
  bindir = RbConfig::CONFIG["bindir"].to_s
  return if bindir.empty?

  ruby_copy = File.join(bindir, base)
  return unless File.exist?(ruby_copy)

  out&.puts("[vcdeps] WARNING: #{base} also ships in #{bindir.tr('/', '\\')} " \
            "— the copy loaded first wins process-wide; preload.rb runs " \
            "first under normal require order.")
end

.win(path) ⇒ Object



156
157
158
# File 'lib/vcdeps/vendor.rb', line 156

def win(path)
  path.nil? ? path : path.tr("/", "\\")
end