Module: Vcvars::Environment
- Defined in:
- lib/vcvars/environment.rb
Overview
Captures the MSVC build environment produced by a vcvars*.bat script and imports it into the current process’s ENV.
vcvars cannot be “sourced” into a running process, so we run it inside a short-lived child cmd.exe, dump the resulting environment with ‘set`, and diff it against the current one. A unique marker line separates vcvars’ banner noise from the ‘set` dump.
Constant Summary collapse
- MARKER =
"___VCVARS_ENV_BEGIN___"- SKIP =
Transient / shell-private variables that must never leak into the parent process. (Compared case-insensitively against upcased names.)
%w[_ PROMPT ERRORLEVEL CMDCMDLINE].freeze
Class Method Summary collapse
-
.activate!(arch: Locator.default_arch, force: false) ⇒ Object
Locate VS, run vcvars, and import the resulting env into ENV.
-
.active? ⇒ Boolean
True if a developer environment is already active in this process.
-
.capture(vcvars: nil, arch: Locator.default_arch) ⇒ Object
Run vcvars in a child process and return the full captured environment as a Hash (does NOT mutate ENV).
- .cl_on_path? ⇒ Boolean
-
.delta(arch: Locator.default_arch) ⇒ Object
The set of variables vcvars adds or changes relative to the current ENV (case-insensitive comparison).
-
.import!(env) ⇒ Object
Import a captured env Hash into ENV: skip transient vars, and only write values that differ (case-insensitively) from the current ENV.
-
.parse_set_output(out) ⇒ Object
Parse the stdout of “<vcvars> ; echo MARKER ; set” into a Hash.
-
.run_capture_batch(vcvars) ⇒ Object
Write a tiny batch that calls vcvars then dumps ‘set`, run it via a clean `cmd /c <file>` (a single unquoted token arg — avoids the embedded-quote escaping that breaks compound cmd lines on Windows), and return stdout.
-
.upcased_snapshot ⇒ Object
— internals ———————————————————–.
Class Method Details
.activate!(arch: Locator.default_arch, force: false) ⇒ Object
Locate VS, run vcvars, and import the resulting env into ENV. Idempotent: returns false (a no-op) when a dev env is already active, true when it actually activated. Pass force: true to re-import anyway.
39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/vcvars/environment.rb', line 39 def activate!(arch: Locator.default_arch, force: false) return false if !force && active? inst = Locator.find(arch: arch) if inst.nil? raise Error, "Could not find Visual Studio with the VC++ tools for " \ "arch #{arch.inspect}. Install the \"Desktop development with C++\" " \ "workload, or confirm vswhere.exe exists at #{Locator::VSWHERE}." end import!(capture(vcvars: inst.vcvars)) true end |
.active? ⇒ Boolean
True if a developer environment is already active in this process.
25 26 27 28 |
# File 'lib/vcvars/environment.rb', line 25 def active? return true if ENV["VSCMD_VER"] && !ENV["VSCMD_VER"].empty? cl_on_path? end |
.capture(vcvars: nil, arch: Locator.default_arch) ⇒ Object
Run vcvars in a child process and return the full captured environment as a Hash (does NOT mutate ENV). Provide an explicit vcvars path to skip location.
66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/vcvars/environment.rb', line 66 def capture(vcvars: nil, arch: Locator.default_arch) vcvars ||= begin inst = Locator.find(arch: arch) raise Error, "Could not locate a vcvars batch script for arch #{arch.inspect}" if inst.nil? inst.vcvars end raise Error, "vcvars script not found: #{vcvars}" unless File.exist?(vcvars) out = run_capture_batch(vcvars) parse_set_output(out) end |
.cl_on_path? ⇒ Boolean
30 31 32 33 34 |
# File 'lib/vcvars/environment.rb', line 30 def cl_on_path? (ENV["PATH"] || "").split(File::PATH_SEPARATOR).any? do |dir| !dir.empty? && File.exist?(File.join(dir, "cl.exe")) end end |
.delta(arch: Locator.default_arch) ⇒ Object
The set of variables vcvars adds or changes relative to the current ENV (case-insensitive comparison). Does NOT mutate ENV. Handy for ‘vcvars env`.
55 56 57 58 59 60 61 |
# File 'lib/vcvars/environment.rb', line 55 def delta(arch: Locator.default_arch) current = upcased_snapshot captured = capture(arch: arch) captured.reject do |k, v| SKIP.include?(k.upcase) || current[k.upcase] == v end end |
.import!(env) ⇒ Object
Import a captured env Hash into ENV: skip transient vars, and only write values that differ (case-insensitively) from the current ENV. Returns the hash. Public for testing.
105 106 107 108 109 110 111 112 |
# File 'lib/vcvars/environment.rb', line 105 def import!(env) current = upcased_snapshot env.each do |k, v| next if SKIP.include?(k.upcase) ENV[k] = v if current[k.upcase] != v end env end |
.parse_set_output(out) ⇒ Object
Parse the stdout of “<vcvars> ; echo MARKER ; set” into a Hash. Public so it can be unit-tested without invoking a real vcvars script.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/vcvars/environment.rb', line 80 def parse_set_output(out) body = out.split(MARKER, 2)[1] raise Error, "vcvars produced no environment (marker missing). " \ "Output head: #{out[0, 200].inspect}" if body.nil? env = {} body.each_line do |raw| line = raw.chomp i = line.index("=") # Skip blank lines and Windows drive pseudo-vars ("=C:", "=ExitCode"). next if i.nil? || i.zero? # Split on the FIRST "=" only — values legitimately contain "=". env[line[0...i]] = line[(i + 1)..] end unless env.keys.any? { |k| k.casecmp("INCLUDE").zero? } raise Error, "vcvars ran but did not set INCLUDE; the MSVC environment " \ "was not initialized (is the C++ workload installed?)." end env end |
.run_capture_batch(vcvars) ⇒ Object
Write a tiny batch that calls vcvars then dumps ‘set`, run it via a clean `cmd /c <file>` (a single unquoted token arg — avoids the embedded-quote escaping that breaks compound cmd lines on Windows), and return stdout.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/vcvars/environment.rb', line 125 def run_capture_batch(vcvars) script = +"@echo off\r\n" script << %{call "#{vcvars}"\r\n} script << "echo #{MARKER}\r\n" script << "set\r\n" path = nil begin file = Tempfile.create(["vcvars_capture", ".bat"]) path = file.path file.write(script) file.close # must be closed before cmd.exe can read it on Windows out, status = Open3.capture2("cmd.exe", "/c", path) unless status.success? raise Error, "vcvars batch exited #{status.exitstatus} (script: #{vcvars})" end out ensure File.unlink(path) if path && File.exist?(path) end end |
.upcased_snapshot ⇒ Object
— internals ———————————————————–
116 117 118 119 120 |
# File 'lib/vcvars/environment.rb', line 116 def upcased_snapshot snap = {} ENV.each { |k, v| snap[k.upcase] = v } snap end |