Module: Vcdeps

Defined in:
lib/vcdeps.rb,
lib/vcdeps/cli.rb,
lib/vcdeps/mkmf.rb,
lib/vcdeps/tool.rb,
lib/vcdeps/doctor.rb,
lib/vcdeps/runner.rb,
lib/vcdeps/vendor.rb,
lib/vcdeps/triplet.rb,
lib/vcdeps/version.rb,
lib/vcdeps/manifest.rb,
lib/vcdeps/bootstrap.rb,
lib/vcdeps/installed.rb

Overview

vcdeps — vcpkg-powered native dependencies for Ruby C extensions on Windows (MSVC). Declare ports in a manifest, get correct mkmf flags and loadable runtime DLLs, no global state. Companion to vcvars (the toolchain) the way vcpkg is companion to MSVC.

# in ext/<gem>/extconf.rb, after require "mkmf":
require "vcdeps/mkmf"
Vcdeps.mkmf!(vendor: File.expand_path("../../lib/foo/vendor", __dir__))

Library use:

require "vcdeps"
inst = Vcdeps.install!(manifest: "ext/foo")
Vcdeps.vendor!(inst, into: "lib/foo/vendor")

Windows MSVC (mswin) Ruby only. Pure Ruby — no compiler required to install.

Defined Under Namespace

Modules: Bootstrap, Doctor, Manifest, Runner, ToolFinder, Triplet, Vendor Classes: BootstrapError, CLI, Error, InstallError, Installed, ManifestError, Port, Tool, ToolNotFound, TripletError

Constant Summary collapse

VERSION =
"0.1.0"

Class Method Summary collapse

Class Method Details

.baseline!(manifest: Dir.pwd, tool: nil, out: $stderr) ⇒ Object

Write/update "builtin-baseline" in \vcpkg.json by running vcpkg x-update-baseline --add-initial-baseline with chdir: manifest. Returns the resulting baseline SHA (re-read from the manifest). Raises ManifestError / ToolNotFound / InstallError.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/vcdeps.rb', line 144

def baseline!(manifest: Dir.pwd, tool: nil, out: $stderr)
  manifest_dir = File.expand_path(manifest)
  path = File.join(manifest_dir, Manifest::MANIFEST_NAME)
  unless File.exist?(path)
    raise ManifestError, "vcdeps: no manifest at #{path.tr('/', '\\')} to add a " \
      "baseline to."
  end

  resolved_tool = tool || tool!(out: out)
  command = [resolved_tool.exe, "x-update-baseline", "--add-initial-baseline"]
  Runner.run(command, env: Runner.child_env(resolved_tool.root),
             chdir: manifest_dir, out: out)

  manifest = Manifest.read_json(path)
  manifest["builtin-baseline"].to_s
end

.bootstrap!(out: $stderr) ⇒ Object

Create the private, registration-free vcpkg instance under \vcpkg (the vcpkg-init mechanism, R§2.4). Idempotent. Raises BootstrapError.



88
89
90
91
# File 'lib/vcdeps.rb', line 88

def bootstrap!(out: $stderr)
  require "vcdeps/bootstrap"
  Bootstrap.run!(out: out)
end

.bypass_installed(prefix, triplet) ⇒ Object

Build an Installed for a --with-vcdeps-dir bypass (tool: nil; prefix taken as-is so include/lib/bin resolve under ; dlls from a bin*.dll glob; ports: []).



66
67
68
69
70
# File 'lib/vcdeps/mkmf.rb', line 66

def bypass_installed(prefix, triplet)
  triplet ||= resolve_triplet_for_bypass
  Installed.new(root: prefix, triplet: triplet, manifest_dir: prefix,
                key: "bypass", tool: nil, prefix: prefix)
end

.default_manifest_dirObject

The extension SOURCE dir: File.expand_path($srcdir) when mkmf defined it (under rake-compiler cwd is tmp//... while vcpkg.json sits beside extconf.rb — $srcdir is the correct anchor), else Dir.pwd.



75
76
77
78
# File 'lib/vcdeps/mkmf.rb', line 75

def default_manifest_dir
  srcdir = (defined?($srcdir) && $srcdir) ? $srcdir : nil
  srcdir ? File.expand_path(srcdir) : Dir.pwd
end

.default_triplet(arch = RbConfig::CONFIG["arch"]) ⇒ Object

Triplet for this Ruby (x64 -> "x64-windows", arm64 -> "arm64-windows", ...). Raises Vcdeps::TripletError for an unrecognized arch.



56
57
58
# File 'lib/vcdeps.rb', line 56

def default_triplet(arch = RbConfig::CONFIG["arch"])
  Triplet.default(arch)
end

.handle_vendor(installed, vendor) ⇒ Object

vendor! when a vendor dir is given; else loudly warn that the built .so will need these DLLs at runtime and the .so's own dir is not searched (R§9.1).



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/vcdeps/mkmf.rb', line 82

def handle_vendor(installed, vendor)
  if vendor
    vendor!(installed, into: vendor)
  elsif !installed.dlls.empty?
    warn <<~MSG
      [vcdeps] WARNING: this build links #{installed.dlls.size} runtime DLL(s)
      that are NOT vendored. The built extension will fail to load at runtime
      ("LoadError: 126: The specified module could not be found") because
      Windows does not search the .so's own directory for dependent DLLs.
      Pass `vendor:` to Vcdeps.mkmf! (so vcdeps copies the DLLs + a preload
      shim into your gem), or build with --with-vcdeps-triplet=x64-windows-static-md
      to link the libraries statically and ship no DLLs.
    MSG
  end
end

.homeObject

The vcdeps state root: ENV or %LOCALAPPDATA%\vcdeps. Deliberately a SHORT path (vcpkg port builds break in deep trees, R§7).



48
49
50
51
52
# File 'lib/vcdeps.rb', line 48

def home
  base = ENV["VCDEPS_HOME"]
  base = File.join(ENV["LOCALAPPDATA"] || Dir.tmpdir, "vcdeps") if base.nil? || base.empty?
  base.tr("/", "\\")
end

.install!(manifest: Dir.pwd, triplet: nil, tool: nil, force: false, out: $stderr) ⇒ Object

Run vcpkg install for \vcpkg.json into \installed<key>. Streams combined output to out (nil to silence). Fast path: a present .vcdeps-complete marker means the install is provably current and vcpkg is not invoked (force: true bypasses it). Triplet precedence: kwarg > ENV > default_triplet.

Raises ManifestError / TripletError / ToolNotFound / InstallError.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/vcdeps.rb', line 100

def install!(manifest: Dir.pwd, triplet: nil, tool: nil, force: false, out: $stderr)
  manifest_dir = File.expand_path(manifest)

  # Pre-flight the manifest BEFORE locating the tool (a missing vcpkg.json or
  # missing baseline must never invoke vcpkg, §5.1/§5.2).
  Manifest.load!(manifest_dir)

  resolved_triplet = resolve_triplet(triplet)
  Triplet.validate!(resolved_triplet)

  resolved_tool = tool || tool!(out: out)

  key = Manifest.key(manifest_dir, resolved_triplet, resolved_tool.version)
  install_root = File.join(home, "installed", key).tr("/", "\\")

  installed = Installed.new(root: install_root, triplet: resolved_triplet,
                            manifest_dir: manifest_dir, key: key, tool: resolved_tool)

  # Fast path: marker present and not forced.
  return installed if !force && Runner.marker_present?(install_root)

  FileUtils.mkdir_p(install_root)
  command = Runner.install_args(resolved_tool.exe, resolved_triplet,
                                manifest_dir, install_root, home)
  Runner.run_install(command, env: Runner.child_env(resolved_tool.root),
                     chdir: manifest_dir, out: out)
  Runner.write_marker!(install_root, key, resolved_tool.version)

  installed
end

.mkmf!(manifest: nil, triplet: nil, vendor: nil, force: false) ⇒ Object

Install the manifest's ports and wire mkmf so the compile/link see the vcpkg tree FIRST (defeating opt-dir shadowing, R§8.4). Behavior in order (§2.4):

1. Vcvars.activate! (idempotent) so mkmf's own try_compile probes work.
2. --with-vcdeps-dir bypass: skip vcpkg, wire a prebuilt <prefix>.
3. install! (manifest defaults to the extension SOURCE dir — $srcdir).
4. PREPEND -I to $INCFLAGS and unshift lib_dir onto $LIBPATH.
5. vendor! when vendor: is given; else warn loudly if DLLs went unvendored.

triplet precedence: kwarg > --with-vcdeps-triplet= > VCDEPS_TRIPLET > default. Returns the Installed. Raises everything install!/vendor! raise.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/vcdeps/mkmf.rb', line 27

def mkmf!(manifest: nil, triplet: nil, vendor: nil, force: false)
  # 1. Toolchain: a Vcvars::Error propagates (no cl == doomed; message names
  #    `vcvars doctor`). No-op when a dev env is already active (CI).
  Vcvars.activate!

  # triplet from --with-vcdeps-triplet (with_config) sits between kwarg and ENV.
  cfg_triplet = mkmf_with_config("vcdeps-triplet")
  resolved_triplet = triplet || (cfg_triplet unless cfg_triplet == true) || nil

  # 2. Bypass: --with-vcdeps-dir=<prefix> wires a prebuilt tree, no vcpkg.
  bypass = mkmf_with_config("vcdeps-dir")
  installed =
    if bypass && bypass != true
      bypass_installed(File.expand_path(bypass), resolved_triplet)
    else
      manifest_dir = manifest || default_manifest_dir
      install!(manifest: manifest_dir, triplet: resolved_triplet, force: force)
    end

  wire_mkmf!(installed)
  handle_vendor(installed, vendor)

  installed
end

.mkmf_quote(path) ⇒ Object

Double-quote a path containing spaces for a naked -I flag (§5.20). Already backslashed by Installed.



113
114
115
# File 'lib/vcdeps/mkmf.rb', line 113

def mkmf_quote(path)
  path.to_s.include?(" ") ? %("#{path}") : path.to_s
end

.mkmf_with_config(name) ⇒ Object

Read a --with-[=value] flag via mkmf's with_config. Returns the value, true (bare flag), or nil/false (absent). Wrapped so a non-mkmf context is tolerated in unit tests.



103
104
105
106
107
108
109
# File 'lib/vcdeps/mkmf.rb', line 103

def mkmf_with_config(name)
  return nil unless respond_to?(:with_config, true) || defined?(with_config)

  with_config(name)
rescue StandardError
  nil
end

.tool(bootstrap: false, out: $stderr) ⇒ Object

Locate a usable vcpkg (§2.3 resolution order). Returns a Tool or nil; with bootstrap: true a miss bootstraps a private instance instead of nil.



62
63
64
# File 'lib/vcdeps.rb', line 62

def tool(bootstrap: false, out: $stderr)
  ToolFinder.find(bootstrap: bootstrap, out: out)
end

.tool!(bootstrap: ENV["VCDEPS_BOOTSTRAP"] == "1", out: $stderr) ⇒ Object

tool(), but raises Vcdeps::ToolNotFound (with the three remedies) on a miss. Default consent for bootstrap comes from ENV so gem install can be unblocked without editing anything.

Raises:



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

def tool!(bootstrap: ENV["VCDEPS_BOOTSTRAP"] == "1", out: $stderr)
  found = tool(bootstrap: bootstrap, out: out)
  return found if found

  raise ToolNotFound, <<~MSG.strip
    vcdeps: no usable vcpkg found. Pick one of:
      (a) Install the VS "vcpkg package manager" component
          (Microsoft.VisualStudio.Component.Vcpkg) — it is Recommended (not
          Required) in the "Desktop development with C++" workload, so a
          cl-capable box can still lack it.
      (b) Set VCPKG_ROOT to an existing vcpkg instance.
      (c) Run `vcdeps bootstrap` (or set VCDEPS_BOOTSTRAP=1 for non-interactive
          installs).
    See `vcdeps doctor` for details.
  MSG
end

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

Sync an Installed's runtime DLLs (+ copyright files, + preload shim) into into (§2.2). SYNC, not copy: owns *.dll, \licenses* and \preload.rb. Returns the list of files written.



134
135
136
137
138
# File 'lib/vcdeps.rb', line 134

def vendor!(installed, into:, licenses: true, shim: true, out: $stderr)
  require "vcdeps/vendor"
  Vendor.sync!(installed, into: File.expand_path(into),
               licenses: licenses, shim: shim, out: out)
end

.wire_mkmf!(installed) ⇒ Object

Wire mkmf: PREPEND the include dir to $INCFLAGS (it precedes $CPPFLAGS on the compile line) and unshift the lib dir onto $LIBPATH (renders before the opt-dir -libpath:). q() double-quotes a spaced path; $LIBPATH quoting is delegated to mkmf's libpathflag (verified to quote spaced paths).



56
57
58
59
60
61
# File 'lib/vcdeps/mkmf.rb', line 56

def wire_mkmf!(installed)
  inc = installed.include_dir
  $INCFLAGS = "-I#{mkmf_quote(inc)} #{$INCFLAGS}"
  $LIBPATH.unshift(installed.lib_dir)
  installed
end