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.("../../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
-
.baseline!(manifest: Dir.pwd, tool: nil, out: $stderr) ⇒ Object
Write/update "builtin-baseline" in
\vcpkg.json by running vcpkg x-update-baseline --add-initial-baselinewith chdir: manifest. -
.bootstrap!(out: $stderr) ⇒ Object
Create the private, registration-free vcpkg instance under
\vcpkg (the vcpkg-init mechanism, R§2.4). -
.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: []). -
.default_manifest_dir ⇒ Object
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. -
.default_triplet(arch = RbConfig::CONFIG["arch"]) ⇒ Object
Triplet for this Ruby (x64 -> "x64-windows", arm64 -> "arm64-windows", ...).
-
.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).
-
.home ⇒ Object
The vcdeps state root: ENV or %LOCALAPPDATA%\vcdeps.
-
.install!(manifest: Dir.pwd, triplet: nil, tool: nil, force: false, out: $stderr) ⇒ Object
Run
vcpkg installfor\vcpkg.json into \installed<key>. -
.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).
-
.mkmf_quote(path) ⇒ Object
Double-quote a path containing spaces for a naked -I flag (§5.20).
-
.mkmf_with_config(name) ⇒ Object
Read a --with-
[=value] flag via mkmf's with_config. -
.tool(bootstrap: false, out: $stderr) ⇒ Object
Locate a usable vcpkg (§2.3 resolution order).
-
.tool!(bootstrap: ENV["VCDEPS_BOOTSTRAP"] == "1", out: $stderr) ⇒ Object
tool(), but raises Vcdeps::ToolNotFound (with the three remedies) on a miss.
-
.vendor!(installed, into:, licenses: true, shim: true, out: $stderr) ⇒ Object
Sync an Installed's runtime DLLs (+ copyright files, + preload shim) into
into(§2.2). -
.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:).
Class Method Details
.baseline!(manifest: Dir.pwd, tool: nil, out: $stderr) ⇒ Object
Write/update "builtin-baseline" in 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.(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
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
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_dir ⇒ Object
The extension SOURCE dir: File.expand_path($srcdir) when mkmf defined it
(under rake-compiler cwd is tmp/
75 76 77 78 |
# File 'lib/vcdeps/mkmf.rb', line 75 def default_manifest_dir srcdir = (defined?($srcdir) && $srcdir) ? $srcdir : nil srcdir ? File.(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 |
.home ⇒ Object
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 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.(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.(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-
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.
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
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.(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 |