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

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.

Returns:

  • (Boolean)


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.

Raises:



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

Returns:

  • (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.

Raises:



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_snapshotObject

— 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