vcvars 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6bb5fa446867714c5eb7e04d78c0cee7a28785239113c15c3e52e2ed35986997
4
+ data.tar.gz: 6cbaa625a124f24b3a374d6a35b74fb53adc27a987b1c7ba0f90443d6d1ca79e
5
+ SHA512:
6
+ metadata.gz: 3dcea9523fe8ed74df9d29add3d0b01ca3258dc87a36dba383a274e0650aa034cc290e7361269fe34cff18d6611c89eb99e155fa63f46a5dcefa32858519c3ff
7
+ data.tar.gz: 2d92d0fd458e2261984e5dc80b69c96abcaa8dd8abd7b302fcafad46530fa656465f84b1029651db7d6681e46d0700562e3dfeca5a8f3c04dbcd65a093fd7fb6
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-05-30
10
+
11
+ ### Added
12
+ - `Vcvars.activate!` — locate Visual Studio via vswhere (with `-find`, plus
13
+ `installationPath`, `VSINSTALLDIR`, and well-known-root fallbacks) and import
14
+ the MSVC environment (`vcvars*.bat`) into the current process. Idempotent.
15
+ - `Vcvars.locate`, `Vcvars.active?`, `Vcvars.env` library helpers.
16
+ - `require "vcvars/rake"` — auto-activates the toolchain so rake-compiler's
17
+ `rake compile` works on an mswin Ruby without a Developer Command Prompt.
18
+ - `vcvars doctor` — diagnoses the common MSVC extension-build failures
19
+ (dev env not loaded, C1083, LNK2019/2001, LNK2005 CRT mismatch, LNK1112 arch
20
+ mismatch, LNK1104, warnings-as-errors, mswin-vs-mingw confusion).
21
+ - `vcvars exec -- <cmd>` — run any command inside the MSVC environment.
22
+ - `vcvars env` — emit the MSVC env delta as bat/powershell/sh/dotenv/json.
23
+ - `vcvars where` — show the located Visual Studio install and vcvars script.
24
+ - `vcvars new NAME` — scaffold a warning-clean, MSVC-ready C-extension gem.
25
+
26
+ [Unreleased]: https://github.com/ned-xvi/vcvars/compare/v0.1.0...HEAD
27
+ [0.1.0]: https://github.com/ned-xvi/vcvars/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ned
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # vcvars
2
+
3
+ **Load the MSVC build environment for native Ruby extensions on Windows — no Developer Command Prompt required.**
4
+
5
+ If you use a [native **MSVC** build of Ruby](https://rubyinstaller.org/) (an
6
+ `x64-mswin64` Ruby, built with `cl.exe` + `nmake`), building C extensions is
7
+ painful: `cl.exe` and `nmake.exe` aren't on your `PATH` until you open a
8
+ "Developer Command Prompt" and run `vcvars64.bat`. Forget that step and you get:
9
+
10
+ ```
11
+ 'nmake' is not recognized as an internal or external command
12
+ ```
13
+
14
+ `vcvars` is to MSVC Ruby what `ridk enable` is to RubyInstaller's MinGW: it
15
+ finds Visual Studio (via `vswhere`), loads the toolchain into your process, and
16
+ gets out of the way. Pure Ruby — **no compiler needed to install it** (which is
17
+ the whole point).
18
+
19
+ > This is the gap nothing else filled: `rake-compiler` hardcodes `nmake` and
20
+ > assumes `cl` is already on `PATH`; `rb_sys`, `ffi-compiler`, and
21
+ > `rake-compiler-dock` are all MinGW/GCC-oriented. None of them activate the
22
+ > MSVC environment for a native `mswin` build.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ gem install vcvars
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ### Build a C extension without a Developer Prompt
33
+
34
+ In a `Rakefile` that uses `rake-compiler`:
35
+
36
+ ```ruby
37
+ require "vcvars/rake" # <- finds VS and loads the MSVC toolchain (mswin only)
38
+ require "rake/extensiontask"
39
+
40
+ spec = Gem::Specification.load("my_ext.gemspec")
41
+ Rake::ExtensionTask.new("my_ext", spec) { |e| e.lib_dir = "lib/my_ext" }
42
+ ```
43
+
44
+ Now `rake compile` just works from an ordinary shell. (`require "vcvars/rake"`
45
+ is a no-op on a MinGW/UCRT Ruby and inside an already-active dev environment.)
46
+
47
+ ### Run any command inside the MSVC environment
48
+
49
+ ```sh
50
+ vcvars exec -- rake compile
51
+ vcvars exec -- nmake
52
+ vcvars exec -- ruby extconf.rb
53
+ ```
54
+
55
+ ### Diagnose a broken toolchain
56
+
57
+ ```sh
58
+ vcvars doctor
59
+ ```
60
+
61
+ ```
62
+ [ OK ] Ruby is a native MSVC (mswin) build
63
+ 3.4.8 x64-mswin64_140 RUBY_SO_NAME=x64-vcruntime140-ruby340
64
+ [INFO] Ruby CRT linkage: MD
65
+ [ OK ] Ruby headers present
66
+ [ OK ] Ruby import library present
67
+ [WARN] Developer environment is NOT active
68
+ cl.exe / nmake.exe are not on PATH ... Use `vcvars exec -- <cmd>` ...
69
+ [ OK ] Visual Studio located
70
+ Visual Studio Community 2026 (18.5...) at C:\Program Files\Microsoft Visual Studio\18\Community
71
+ [ OK ] vcvars activation works (cl.exe resolvable)
72
+ ```
73
+
74
+ `doctor` knows the usual suspects and how to fix each: dev env not loaded,
75
+ `C1083` (missing `ruby.h`), `LNK2019/LNK2001` (unresolved externals),
76
+ `LNK2005` (CRT `/MD` vs `/MT` mismatch), `LNK1112` (x64/x86 arch mismatch),
77
+ `LNK1104` (`LIB` unset), warnings-promoted-to-errors, and mswin-vs-mingw mix-ups.
78
+
79
+ ### Scaffold a new MSVC-ready extension gem
80
+
81
+ ```sh
82
+ vcvars new my_ext
83
+ cd my_ext
84
+ bundle install
85
+ rake compile && rake test
86
+ ```
87
+
88
+ The generated gem has a correct, portable `extconf.rb`, a warning-clean sample
89
+ `.c` (clean under Ruby's aggressive `-we` flags), a `Rakefile` already wired to
90
+ `vcvars/rake`, and a passing test.
91
+
92
+ ### Emit the environment for your shell
93
+
94
+ ```powershell
95
+ vcvars env --format powershell | Invoke-Expression
96
+ ```
97
+
98
+ ```sh
99
+ vcvars env --format bat > vcenv.bat && call vcenv.bat # cmd.exe
100
+ vcvars env --format json # machine-readable
101
+ ```
102
+
103
+ ## Library API
104
+
105
+ ```ruby
106
+ require "vcvars"
107
+
108
+ Vcvars.mswin? # => true on a native MSVC Ruby
109
+ Vcvars.active? # => is a developer environment already loaded in this process?
110
+ Vcvars.activate! # locate VS + import the MSVC env into ENV (idempotent); => true/false
111
+ Vcvars.locate # => Vcvars::Locator::Installation (vs_path, vcvars, arch, version, name)
112
+ Vcvars.env # => Hash of the env vars vcvars adds/changes (no ENV mutation)
113
+
114
+ # Target a different toolchain arch:
115
+ Vcvars.activate!(arch: "arm64")
116
+ ```
117
+
118
+ ## How it works
119
+
120
+ `vcvars` runs the located `vcvars*.bat` inside a short-lived `cmd.exe`, dumps the
121
+ resulting environment with `set` (separated from banner noise by a marker line),
122
+ diffs it against the current environment, and imports the new/changed variables
123
+ (`PATH`, `INCLUDE`, `LIB`, `LIBPATH`, the `VSCMD_*`/`VC*` markers, …) into `ENV`.
124
+ rake-compiler shells out with `FileUtils#sh`, which inherits that `ENV`, so a
125
+ single activation covers the `extconf.rb`, `nmake`, and `nmake install` steps.
126
+
127
+ Activation is idempotent (it no-ops when `VSCMD_VER` is set or `cl.exe` is
128
+ already on `PATH`), so it's safe to call from every Rake invocation.
129
+
130
+ ## Requirements
131
+
132
+ - Windows with a native **MSVC** Ruby (`x64-mswin64` / `RUBY_SO_NAME` containing
133
+ `vcruntime`). On a MinGW/UCRT Ruby, use RubyInstaller's `ridk enable` instead.
134
+ - Visual Studio 2017+ or the Build Tools with the **Desktop development with
135
+ C++** workload (provides `cl.exe`, `nmake.exe`, and `vcvars*.bat`).
136
+
137
+ ## License
138
+
139
+ [MIT](LICENSE.txt).
data/exe/vcvars ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "vcvars/cli"
5
+
6
+ exit Vcvars::CLI.start(ARGV)
data/lib/vcvars/cli.rb ADDED
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vcvars"
4
+
5
+ module Vcvars
6
+ # Command-line interface for the `vcvars` executable.
7
+ class CLI
8
+ def self.start(argv)
9
+ new.run(argv)
10
+ end
11
+
12
+ # Returns a process exit status (Integer).
13
+ def run(argv)
14
+ argv = argv.dup
15
+ command = argv.shift
16
+
17
+ case command
18
+ when "doctor" then cmd_doctor(argv)
19
+ when "where" then cmd_where(argv)
20
+ when "env" then cmd_env(argv)
21
+ when "exec" then cmd_exec(argv)
22
+ when "new" then cmd_new(argv)
23
+ when "version", "-v", "--version" then cmd_version
24
+ when nil, "help", "-h", "--help" then print_help; 0
25
+ else
26
+ warn "vcvars: unknown command #{command.inspect}\n\n"
27
+ print_help
28
+ 1
29
+ end
30
+ rescue Vcvars::Error, ArgumentError => e
31
+ # ArgumentError covers user input like an unsupported `--arch` value, which
32
+ # Locator.vcvars_name raises; report it the same clean way as other errors.
33
+ warn "vcvars: #{e.message}"
34
+ 1
35
+ rescue Interrupt
36
+ 130
37
+ end
38
+
39
+ # --- commands ------------------------------------------------------------
40
+
41
+ def cmd_version
42
+ puts "vcvars #{Vcvars::VERSION}"
43
+ 0
44
+ end
45
+
46
+ def cmd_doctor(argv)
47
+ require "vcvars/doctor"
48
+ deep = !argv.include?("--quick")
49
+ checks = Doctor.run(deep: deep)
50
+
51
+ checks.each do |c|
52
+ puts "#{c.icon} #{c.label}"
53
+ next unless c.detail && !c.detail.to_s.empty?
54
+
55
+ c.detail.to_s.each_line { |line| puts " #{line.chomp}" }
56
+ end
57
+
58
+ fails = checks.count { |c| c.status == :fail }
59
+ warns = checks.count { |c| c.status == :warn }
60
+ puts
61
+ if fails.zero?
62
+ extra = warns.positive? ? " (#{warns} warning#{'s' if warns > 1})" : ""
63
+ puts "Summary: no blocking problems#{extra}."
64
+ 0
65
+ else
66
+ puts "Summary: #{fails} problem#{'s' if fails > 1} found — see remedies above."
67
+ 1
68
+ end
69
+ end
70
+
71
+ def cmd_where(argv)
72
+ arch = extract_opt(argv, "--arch")
73
+ inst = arch ? Vcvars.locate(arch: arch) : Vcvars.locate
74
+ if inst
75
+ puts inst.to_s
76
+ puts "vcvars script: #{inst.vcvars}"
77
+ puts "target arch: #{inst.arch}"
78
+ 0
79
+ else
80
+ warn "vcvars: no Visual Studio with the C++ tools was found."
81
+ warn "Install the \"Desktop development with C++\" workload, then retry."
82
+ 1
83
+ end
84
+ end
85
+
86
+ def cmd_env(argv)
87
+ format = (extract_opt(argv, "--format") || "bat").downcase
88
+ arch = extract_opt(argv, "--arch")
89
+ delta = arch ? Vcvars.env(arch: arch) : Vcvars.env
90
+ print_env(delta, format)
91
+ 0
92
+ end
93
+
94
+ def cmd_exec(argv)
95
+ arch, command = parse_exec_args(argv)
96
+ if command.nil? || command.empty?
97
+ warn "vcvars exec: no command given."
98
+ warn "Example: vcvars exec -- rake compile"
99
+ return 1
100
+ end
101
+
102
+ arch ? Vcvars.activate!(arch: arch) : Vcvars.activate!
103
+
104
+ ok = system(*command)
105
+ if ok.nil?
106
+ warn "vcvars exec: failed to run #{command.first.inspect} (not found?)."
107
+ return 127
108
+ end
109
+ $?.exitstatus || (ok ? 0 : 1)
110
+ end
111
+
112
+ def cmd_new(argv)
113
+ require "vcvars/scaffold"
114
+ force = argv.delete("--force") ? true : false
115
+ dir = extract_opt(argv, "--dir")
116
+ name = argv.shift
117
+
118
+ unless name
119
+ warn "vcvars new: missing NAME."
120
+ warn "Example: vcvars new my_ext"
121
+ return 1
122
+ end
123
+
124
+ scaffold = Scaffold.new(name, dir: dir)
125
+ created = scaffold.generate(force: force)
126
+
127
+ puts "Created gem '#{scaffold.name}' (module #{scaffold.module_name}) in #{scaffold.dest}:"
128
+ prefix = scaffold.dest + File::SEPARATOR
129
+ created.each { |f| puts " #{f.start_with?(prefix) ? f[prefix.length..] : f}" }
130
+ puts
131
+ puts "Next steps:"
132
+ puts " cd #{scaffold.dest}"
133
+ puts " bundle install"
134
+ puts " rake compile && rake test"
135
+ 0
136
+ end
137
+
138
+ # --- helpers -------------------------------------------------------------
139
+
140
+ # Splits exec args into [arch, command]. Flags (a leading --arch, or
141
+ # anything before a "--" separator) are parsed; the rest is the command.
142
+ def parse_exec_args(argv)
143
+ if (sep = argv.index("--"))
144
+ head = argv[0...sep]
145
+ command = argv[(sep + 1)..]
146
+ [extract_opt(head, "--arch"), command]
147
+ else
148
+ rest = argv.dup
149
+ arch = nil
150
+ # Only honor flags that appear before the command.
151
+ if rest.first == "--arch"
152
+ rest.shift
153
+ arch = rest.shift
154
+ elsif rest.first.to_s.start_with?("--arch=")
155
+ arch = rest.shift.split("=", 2)[1]
156
+ end
157
+ [arch, rest]
158
+ end
159
+ end
160
+
161
+ # Pulls "--opt value" or "--opt=value" out of argv (mutating it). Returns
162
+ # the value or nil.
163
+ def extract_opt(argv, name)
164
+ if (i = argv.index(name))
165
+ value = argv[i + 1]
166
+ if value.nil?
167
+ argv.slice!(i, 1)
168
+ raise Error, "#{name} requires a value."
169
+ end
170
+ argv.slice!(i, 2)
171
+ return value
172
+ end
173
+ prefix = "#{name}="
174
+ if (i = argv.index { |a| a.to_s.start_with?(prefix) })
175
+ value = argv[i][prefix.length..]
176
+ argv.slice!(i, 1)
177
+ return value
178
+ end
179
+ nil
180
+ end
181
+
182
+ def print_env(delta, format)
183
+ keys = delta.keys.sort_by(&:upcase)
184
+ case format
185
+ when "bat", "cmd"
186
+ keys.each { |k| puts %{set "#{k}=#{delta[k]}"} }
187
+ when "powershell", "ps", "ps1"
188
+ keys.each { |k| puts %{$env:#{k} = '#{delta[k].to_s.gsub("'", "''")}'} }
189
+ when "sh", "bash"
190
+ keys.each { |k| puts %{export #{k}="#{sh_escape(delta[k])}"} }
191
+ when "dotenv", "env"
192
+ keys.each { |k| puts "#{k}=#{delta[k]}" }
193
+ when "json"
194
+ require "json"
195
+ puts JSON.pretty_generate(delta)
196
+ else
197
+ raise Error, "unknown --format #{format.inspect} " \
198
+ "(use bat, powershell, sh, dotenv, or json)."
199
+ end
200
+ end
201
+
202
+ def sh_escape(value)
203
+ value.to_s.gsub(/[\\"$`]/) { |c| "\\#{c}" }
204
+ end
205
+
206
+ def print_help
207
+ puts <<~HELP
208
+ vcvars #{Vcvars::VERSION} — load the MSVC build environment for native Ruby extensions.
209
+
210
+ Usage:
211
+ vcvars doctor [--quick] Diagnose the MSVC extension toolchain
212
+ vcvars where [--arch ARCH] Show the located Visual Studio + vcvars script
213
+ vcvars env [--arch ARCH] [--format bat|powershell|sh|dotenv|json]
214
+ Print the MSVC env vars (the vcvars delta)
215
+ vcvars exec [--arch ARCH] -- CMD [ARGS...]
216
+ Run CMD inside the MSVC environment
217
+ vcvars new NAME [--dir DIR] [--force]
218
+ Scaffold a new MSVC-ready C-extension gem
219
+ vcvars version
220
+ vcvars help
221
+
222
+ Examples:
223
+ vcvars doctor
224
+ vcvars exec -- rake compile
225
+ vcvars exec -- nmake
226
+ vcvars env --format powershell | Invoke-Expression
227
+ vcvars new my_ext
228
+
229
+ In a Rakefile, `require "vcvars/rake"` before Rake::ExtensionTask.new to
230
+ auto-load the toolchain so `rake compile` works without a Developer Prompt.
231
+ HELP
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "rbconfig"
5
+ require "vcvars/locator"
6
+ require "vcvars/environment"
7
+
8
+ module Vcvars
9
+ # Diagnoses the common reasons a native C-extension build fails under an MSVC
10
+ # (mswin) Ruby on Windows, and explains how to fix each one. The checks are
11
+ # derived from the actual RbConfig of the running Ruby plus live probes of the
12
+ # toolchain, so the advice is specific rather than generic.
13
+ module Doctor
14
+ module_function
15
+
16
+ Check = Struct.new(:status, :label, :detail, keyword_init: true) do
17
+ # :ok | :warn | :fail | :info
18
+ def icon
19
+ { ok: "[ OK ]", warn: "[WARN]", fail: "[FAIL]", info: "[INFO]" }[status]
20
+ end
21
+ end
22
+
23
+ # ---- RbConfig-based primitives (cheap, no shell-out) --------------------
24
+
25
+ def mswin?
26
+ RbConfig::CONFIG["target_os"].to_s =~ /mswin/ ? true : false
27
+ end
28
+
29
+ # :mswin | :mingw_ucrt | :mingw_msvcrt | :other
30
+ def toolchain
31
+ os = RbConfig::CONFIG["target_os"].to_s
32
+ so = RbConfig::CONFIG["RUBY_SO_NAME"].to_s
33
+ return :mswin if os =~ /mswin/
34
+ return :mingw_ucrt if so =~ /ucrt/i || os =~ /ucrt/i
35
+ return :mingw_msvcrt if os =~ /mingw/
36
+ :other
37
+ end
38
+
39
+ # CRT linkage Ruby itself was built with: :MD | :MDd | :MT | :MTd | :unknown
40
+ def crt_flag(cflags = RbConfig::CONFIG["CFLAGS"].to_s)
41
+ case cflags
42
+ when %r{(?:^|\s)[-/]MDd\b} then :MDd
43
+ when %r{(?:^|\s)[-/]MTd\b} then :MTd
44
+ when %r{(?:^|\s)[-/]MD\b} then :MD
45
+ when %r{(?:^|\s)[-/]MT\b} then :MT
46
+ else :unknown
47
+ end
48
+ end
49
+
50
+ # Target machine the linker must produce: :x64 | :x86 | :arm64 | :unknown
51
+ def arch
52
+ a = RbConfig::CONFIG["arch"].to_s
53
+ return :x64 if a =~ /\A(?:x64|x86_64|amd64)/i
54
+ return :arm64 if a =~ /arm64|aarch64/i
55
+ return :x86 if a =~ /\A(?:i[3-6]86|x86)/i
56
+ :unknown
57
+ end
58
+
59
+ # If the given extension flags use a static CRT while Ruby uses a dynamic
60
+ # one (or vice-versa), return the offending flag (:MT/:MTd/:MD/:MDd); else nil.
61
+ def crt_conflict(ext_cflags, ruby_crt = crt_flag)
62
+ return nil if ext_cflags.to_s.empty?
63
+ ruby_dynamic = %i[MD MDd].include?(ruby_crt)
64
+ if ruby_dynamic && ext_cflags =~ %r{(?:^|\s)[-/]MT(d?)\b}
65
+ Regexp.last_match(1) == "d" ? :MTd : :MT
66
+ elsif !ruby_dynamic && ext_cflags =~ %r{(?:^|\s)[-/]MD(d?)\b}
67
+ Regexp.last_match(1) == "d" ? :MDd : :MD
68
+ end
69
+ end
70
+
71
+ # Warnings Ruby promotes to hard errors, e.g. ["-we4028", "-we4047"].
72
+ def werror_flags(cflags = RbConfig::CONFIG["CFLAGS"].to_s)
73
+ cflags.scan(/-we\d{4}/).uniq
74
+ end
75
+
76
+ def import_lib_path
77
+ File.join(RbConfig::CONFIG["libdir"], RbConfig::CONFIG["LIBRUBYARG_SHARED"].to_s)
78
+ end
79
+
80
+ def ruby_header_path
81
+ File.join(RbConfig::CONFIG["rubyhdrdir"].to_s, "ruby.h")
82
+ end
83
+
84
+ # `where <tool>` prints "INFO: Could not find files..." and exits non-zero
85
+ # when a tool is absent. Returns the resolved path, or nil.
86
+ def which(tool)
87
+ out, status = Open3.capture2("where", tool)
88
+ return nil unless status.success?
89
+ line = out.lines.map(&:strip).find { |l| !l.empty? && l !~ /Could not find/ }
90
+ line
91
+ rescue StandardError
92
+ nil
93
+ end
94
+
95
+ # ---- The diagnostic run --------------------------------------------------
96
+
97
+ # Returns an Array<Check>. When deep: true, also locates VS and tries a real
98
+ # environment capture (so it can confirm a build would actually find cl).
99
+ def run(deep: true)
100
+ checks = []
101
+ checks.concat(ruby_checks)
102
+ checks.concat(header_checks)
103
+ checks.concat(env_checks)
104
+ checks.concat(vs_checks(deep: deep))
105
+ checks.concat(hygiene_checks)
106
+ checks
107
+ end
108
+
109
+ def ruby_checks
110
+ ver = "#{RUBY_VERSION} #{RbConfig::CONFIG['arch']}"
111
+ tc = toolchain
112
+ out = []
113
+ out << if mswin?
114
+ Check.new(status: :ok, label: "Ruby is a native MSVC (mswin) build",
115
+ detail: "#{ver} RUBY_SO_NAME=#{RbConfig::CONFIG['RUBY_SO_NAME']}")
116
+ else
117
+ Check.new(status: :warn, label: "Ruby is NOT an mswin build (#{tc})",
118
+ detail: "vcvars targets the MSVC (mswin) Ruby. On a MinGW/UCRT " \
119
+ "Ruby use RubyInstaller's `ridk enable` instead — the MSVC " \
120
+ "toolchain is ABI-incompatible with this build.")
121
+ end
122
+ out << Check.new(status: :info, label: "Ruby CRT linkage: #{crt_flag}",
123
+ detail: "Extensions must use the same CRT. mkmf inherits this " \
124
+ "automatically — never force /MT or /MTd.")
125
+ we = werror_flags
126
+ unless we.empty?
127
+ out << Check.new(status: :info, label: "Warnings promoted to errors: #{we.join(' ')}",
128
+ detail: "Missing prototypes (C4013) and pointer/type mismatches " \
129
+ "(C4047/C4028) will hard-fail. Include the right headers; " \
130
+ "do not strip these flags.")
131
+ end
132
+ out
133
+ end
134
+
135
+ def header_checks
136
+ out = []
137
+ out << if File.exist?(ruby_header_path)
138
+ Check.new(status: :ok, label: "Ruby headers present", detail: ruby_header_path)
139
+ else
140
+ Check.new(status: :fail, label: "ruby.h NOT found",
141
+ detail: "Expected at #{ruby_header_path}. C1083 'Cannot open include " \
142
+ "file' results. Reinstall Ruby's dev headers, and always run " \
143
+ "`ruby extconf.rb` (which injects the -I paths) before nmake.")
144
+ end
145
+ out << if File.exist?(import_lib_path)
146
+ Check.new(status: :ok, label: "Ruby import library present", detail: import_lib_path)
147
+ else
148
+ Check.new(status: :fail, label: "Ruby import library NOT found",
149
+ detail: "Expected #{import_lib_path}. Missing it causes LNK2019/LNK1104. " \
150
+ "Link through mkmf, which adds it via LIBRUBYARG.")
151
+ end
152
+ out
153
+ end
154
+
155
+ def env_checks
156
+ out = []
157
+ if Environment.active?
158
+ cl = which("cl") || "(on PATH)"
159
+ out << Check.new(status: :ok, label: "Developer environment is ACTIVE in this process",
160
+ detail: "cl: #{cl}")
161
+ out << if (ENV["LIB"] || "").strip.empty?
162
+ Check.new(status: :warn, label: "LIB is empty despite an active dev env",
163
+ detail: "The linker may fail with LNK1104. Re-activate with a full vcvars.")
164
+ else
165
+ Check.new(status: :ok, label: "LIB / library search path is set", detail: nil)
166
+ end
167
+ if ENV["VSCMD_ARG_TGT_ARCH"] && !ENV["VSCMD_ARG_TGT_ARCH"].casecmp?(arch.to_s)
168
+ out << Check.new(status: :fail,
169
+ label: "Toolchain arch (#{ENV['VSCMD_ARG_TGT_ARCH']}) != Ruby arch (#{arch})",
170
+ detail: "Causes LNK1112. For #{arch} Ruby use vcvars64.bat, not vcvars32.bat.")
171
+ end
172
+ else
173
+ out << Check.new(status: :warn, label: "Developer environment is NOT active",
174
+ detail: "cl.exe / nmake.exe are not on PATH, so `rake compile` / " \
175
+ "`gem install <native>` will fail with \"'nmake' is not " \
176
+ "recognized\". Use `vcvars exec -- <cmd>`, `require \"vcvars/rake\"` " \
177
+ "in your Rakefile, or `vcvars env` to load it.")
178
+ end
179
+ out
180
+ end
181
+
182
+ def vs_checks(deep:)
183
+ out = []
184
+ inst = Locator.find
185
+ if inst.nil?
186
+ out << Check.new(status: :fail, label: "No Visual Studio with C++ tools found",
187
+ detail: "Looked via vswhere (#{Locator::VSWHERE}) and known install " \
188
+ "roots. Install the \"Desktop development with C++\" workload.")
189
+ return out
190
+ end
191
+
192
+ out << Check.new(status: :ok, label: "Visual Studio located", detail: inst.to_s)
193
+ out << Check.new(status: :info, label: "vcvars script", detail: inst.vcvars)
194
+
195
+ return out unless deep
196
+
197
+ begin
198
+ captured = Environment.capture(vcvars: inst.vcvars)
199
+ cl_dir = (captured.find { |k, _| k.casecmp("PATH").zero? }&.last || "")
200
+ .split(File::PATH_SEPARATOR).find { |d| !d.empty? && File.exist?(File.join(d, "cl.exe")) }
201
+ out << if cl_dir
202
+ Check.new(status: :ok, label: "vcvars activation works (cl.exe resolvable)",
203
+ detail: File.join(cl_dir, "cl.exe"))
204
+ else
205
+ Check.new(status: :warn, label: "vcvars ran but cl.exe was not found on the new PATH",
206
+ detail: "The C++ toolset may not be installed for this arch.")
207
+ end
208
+ rescue Error => e
209
+ out << Check.new(status: :fail, label: "vcvars activation failed", detail: e.message)
210
+ end
211
+ out
212
+ end
213
+
214
+ def hygiene_checks
215
+ out = []
216
+ # A GNU `link` (from Git/MSYS) on PATH shadows the MSVC linker and causes
217
+ # baffling link failures.
218
+ link = which("link")
219
+ if link && link =~ %r{[\\/](Git|usr|msys|mingw)[\\/]}i
220
+ out << Check.new(status: :warn, label: "A non-MSVC `link` is on PATH",
221
+ detail: "#{link} is the GNU coreutils `link`, not the MSVC linker. " \
222
+ "Inside an active dev env the MSVC link.exe should win; if you " \
223
+ "see strange linker errors, check PATH order.")
224
+ end
225
+
226
+ # User-injected CRT flags that would conflict with Ruby's CRT.
227
+ ext_flags = "#{ENV['CL']} #{ENV['CFLAGS']}".strip
228
+ if (bad = crt_conflict(ext_flags))
229
+ out << Check.new(status: :fail, label: "CRT conflict in CL/CFLAGS: #{bad}",
230
+ detail: "Ruby uses #{crt_flag}; your environment forces #{bad}. " \
231
+ "This causes LNK2005 'already defined' and runtime heap " \
232
+ "corruption. Remove /#{bad} from the CL and CFLAGS env vars.")
233
+ end
234
+ out
235
+ end
236
+
237
+ # Convenience: did the run surface any hard failure?
238
+ def healthy?(checks)
239
+ checks.none? { |c| c.status == :fail }
240
+ end
241
+ end
242
+ end