Module: Vcvars::Locator
- Defined in:
- lib/vcvars/locator.rb
Overview
Finds a Visual Studio / Build Tools installation and the arch-specific vcvars batch script that activates the MSVC build environment.
Strategy (most robust first):
1. `vswhere -find VC\Auxiliary\Build\<vcvars>.bat` — returns the script
path directly. This is used as the primary method because on some VS
layouts (observed on the VS "18" / 2026 line) `-property
installationPath` comes back empty, while `-find` still works.
2. `vswhere -property installationPath` + the known relative path.
3. `VSINSTALLDIR` (set when already inside a developer environment).
4. Scan well-known install roots (Program Files\Microsoft Visual Studio).
Defined Under Namespace
Classes: Installation
Constant Summary collapse
- VSWHERE =
File.join( ENV["ProgramFiles(x86)"] || 'C:\Program Files (x86)', "Microsoft Visual Studio", "Installer", "vswhere.exe" ).freeze
- VSWHERE_SELECT =
Selects the newest STABLE install carrying the x64/x86 VC++ toolset. Prerelease/Insiders installs are only considered as a fallback (find retries with “-prerelease” appended), so a stable VS is preferred when both are present.
[ "-latest", "-products", "*", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" ].freeze
Class Method Summary collapse
-
.default_arch ⇒ Object
The arch this Ruby was built for, e.g.
-
.find(arch: default_arch) ⇒ Object
Returns an Installation, or nil if no suitable VS install is found.
- .first_nonempty(*values) ⇒ Object
- .resolve(arch) ⇒ Object
-
.resolve_via_env(name) ⇒ Object
The vcvars*.bat under VSINSTALLDIR (set inside an active dev env), or nil.
-
.resolve_via_vswhere(select, name, arch) ⇒ Object
Resolve a complete Installation from ONE vswhere selection, so the vcvars path and the reported metadata (version/name) describe the SAME install.
- .scan_known_roots(name) ⇒ Object
-
.vcvars_name(arch) ⇒ Object
Map a Ruby/target arch to the matching vcvars script name.
-
.vs_root_from(bat) ⇒ Object
The install root is four levels up from <root>VCAuxiliaryBuildx.bat.
-
.vswhere(args) ⇒ Object
Run vswhere with the given args; returns stdout (“” on any failure).
-
.win(path) ⇒ Object
Normalize to native Windows separators for clean display (File.join mixes “/” into the backslash paths vswhere returns).
Class Method Details
.default_arch ⇒ Object
The arch this Ruby was built for, e.g. “x64-mswin64_140”.
42 43 44 |
# File 'lib/vcvars/locator.rb', line 42 def default_arch RbConfig::CONFIG["arch"] end |
.find(arch: default_arch) ⇒ Object
Returns an Installation, or nil if no suitable VS install is found. Memoized per arch within a process (the install location is stable).
61 62 63 64 65 66 67 |
# File 'lib/vcvars/locator.rb', line 61 def find(arch: default_arch) @cache ||= {} key = arch.to_s return @cache[key] if @cache.key?(key) @cache[key] = resolve(arch) end |
.first_nonempty(*values) ⇒ Object
135 136 137 |
# File 'lib/vcvars/locator.rb', line 135 def first_nonempty(*values) values.find { |v| v && !v.empty? } end |
.resolve(arch) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/vcvars/locator.rb', line 69 def resolve(arch) name = vcvars_name(arch) if File.exist?(VSWHERE) # Prefer a stable install; only then consider prerelease/Insiders. [VSWHERE_SELECT, VSWHERE_SELECT + ["-prerelease"]].each do |select| inst = resolve_via_vswhere(select, name, arch) return inst if inst end end bat = resolve_via_env(name) || scan_known_roots(name) return nil unless bat Installation.new(vs_path: win(vs_root_from(bat)), vcvars: win(bat), arch: arch.to_s, version: nil, name: nil) end |
.resolve_via_env(name) ⇒ Object
The vcvars*.bat under VSINSTALLDIR (set inside an active dev env), or nil.
127 128 129 130 131 132 133 |
# File 'lib/vcvars/locator.rb', line 127 def resolve_via_env(name) vsdir = ENV["VSINSTALLDIR"] return nil if vsdir.nil? || vsdir.empty? cand = File.join(vsdir, "VC", "Auxiliary", "Build", name) File.exist?(cand) ? cand : nil end |
.resolve_via_vswhere(select, name, arch) ⇒ Object
Resolve a complete Installation from ONE vswhere selection, so the vcvars path and the reported metadata (version/name) describe the SAME install.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/vcvars/locator.rb', line 89 def resolve_via_vswhere(select, name, arch) path = vswhere(select + ["-property", "installationPath"]).strip bat = nil unless path.empty? cand = File.join(path, "VC", "Auxiliary", "Build", name) bat = cand if File.exist?(cand) end # Some VS layouts return an empty installationPath; -find yields the path. if bat.nil? found = vswhere(select + ["-find", "VC\\Auxiliary\\Build\\#{name}"]) bat = found.lines.map(&:strip).find { |l| !l.empty? && File.exist?(l) } return nil unless bat path = vs_root_from(bat) end version = first_nonempty( vswhere(select + ["-property", "installationVersion"]).strip, vswhere(select + ["-property", "catalog_productDisplayVersion"]).strip ) display = vswhere(select + ["-property", "displayName"]).strip Installation.new( vs_path: win(path), vcvars: win(bat), arch: arch.to_s, version: version, name: display.empty? ? nil : display ) end |
.scan_known_roots(name) ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/vcvars/locator.rb', line 139 def scan_known_roots(name) bases = [ENV["ProgramFiles"], ENV["ProgramFiles(x86)"]].compact.uniq # VS 2026 uses a numeric "18" directory; 2022/2019/2017 use the year. versions = %w[18 2022 2019 2017] editions = %w[Preview Enterprise Professional Community BuildTools] bases.each do |pf| root = File.join(pf, "Microsoft Visual Studio") versions.each do |ver| editions.each do |ed| cand = File.join(root, ver, ed, "VC", "Auxiliary", "Build", name) return cand if File.exist?(cand) end end end nil end |
.vcvars_name(arch) ⇒ Object
Map a Ruby/target arch to the matching vcvars script name. The build host is assumed to be x64 (overwhelmingly true for Ruby on Windows), so arm64 and x86 targets use the x64-hosted cross scripts.
49 50 51 52 53 54 55 56 57 |
# File 'lib/vcvars/locator.rb', line 49 def vcvars_name(arch) case arch.to_s.downcase when "x64", "amd64", "x86_64", /\Ax64\b/, /x86_64/, /amd64/ then "vcvars64.bat" when "arm64", "aarch64", /arm64/, /aarch64/ then "vcvarsamd64_arm64.bat" when "x86", "i386", "i686", /\Ai\d86/, /\Ax86\b/ then "vcvars32.bat" else raise ArgumentError, "unsupported arch for vcvars: #{arch.inspect}" end end |
.vs_root_from(bat) ⇒ Object
The install root is four levels up from <root>VCAuxiliaryBuildx.bat.
157 158 159 |
# File 'lib/vcvars/locator.rb', line 157 def vs_root_from(bat) File.dirname(File.dirname(File.dirname(File.dirname(bat)))) end |
.vswhere(args) ⇒ Object
Run vswhere with the given args; returns stdout (“” on any failure). Args are passed as a real argv (no shell), so the literal “*” in “-products *” reaches vswhere instead of being glob-expanded.
164 165 166 167 168 169 |
# File 'lib/vcvars/locator.rb', line 164 def vswhere(args) out, status = Open3.capture2(VSWHERE, *args) status.success? ? out : "" rescue StandardError "" end |
.win(path) ⇒ Object
Normalize to native Windows separators for clean display (File.join mixes “/” into the backslash paths vswhere returns). Harmless for File.* / cmd.
122 123 124 |
# File 'lib/vcvars/locator.rb', line 122 def win(path) path.nil? ? path : path.tr("/", "\\") end |