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 +7 -0
- data/CHANGELOG.md +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +139 -0
- data/exe/vcvars +6 -0
- data/lib/vcvars/cli.rb +234 -0
- data/lib/vcvars/doctor.rb +242 -0
- data/lib/vcvars/environment.rb +148 -0
- data/lib/vcvars/locator.rb +171 -0
- data/lib/vcvars/rake.rb +42 -0
- data/lib/vcvars/scaffold.rb +324 -0
- data/lib/vcvars/version.rb +5 -0
- data/lib/vcvars.rb +48 -0
- metadata +94 -0
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
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
|