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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "vcvars/locator"
|
|
6
|
+
|
|
7
|
+
module Vcvars
|
|
8
|
+
# Captures the MSVC build environment produced by a vcvars*.bat script and
|
|
9
|
+
# imports it into the current process's ENV.
|
|
10
|
+
#
|
|
11
|
+
# vcvars cannot be "sourced" into a running process, so we run it inside a
|
|
12
|
+
# short-lived child cmd.exe, dump the resulting environment with `set`, and
|
|
13
|
+
# diff it against the current one. A unique marker line separates vcvars'
|
|
14
|
+
# banner noise from the `set` dump.
|
|
15
|
+
module Environment
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
MARKER = "___VCVARS_ENV_BEGIN___"
|
|
19
|
+
|
|
20
|
+
# Transient / shell-private variables that must never leak into the parent
|
|
21
|
+
# process. (Compared case-insensitively against upcased names.)
|
|
22
|
+
SKIP = %w[_ PROMPT ERRORLEVEL CMDCMDLINE].freeze
|
|
23
|
+
|
|
24
|
+
# True if a developer environment is already active in this process.
|
|
25
|
+
def active?
|
|
26
|
+
return true if ENV["VSCMD_VER"] && !ENV["VSCMD_VER"].empty?
|
|
27
|
+
cl_on_path?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cl_on_path?
|
|
31
|
+
(ENV["PATH"] || "").split(File::PATH_SEPARATOR).any? do |dir|
|
|
32
|
+
!dir.empty? && File.exist?(File.join(dir, "cl.exe"))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Locate VS, run vcvars, and import the resulting env into ENV.
|
|
37
|
+
# Idempotent: returns false (a no-op) when a dev env is already active,
|
|
38
|
+
# true when it actually activated. Pass force: true to re-import anyway.
|
|
39
|
+
def activate!(arch: Locator.default_arch, force: false)
|
|
40
|
+
return false if !force && active?
|
|
41
|
+
|
|
42
|
+
inst = Locator.find(arch: arch)
|
|
43
|
+
if inst.nil?
|
|
44
|
+
raise Error, "Could not find Visual Studio with the VC++ tools for " \
|
|
45
|
+
"arch #{arch.inspect}. Install the \"Desktop development with C++\" " \
|
|
46
|
+
"workload, or confirm vswhere.exe exists at #{Locator::VSWHERE}."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
import!(capture(vcvars: inst.vcvars))
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The set of variables vcvars adds or changes relative to the current ENV
|
|
54
|
+
# (case-insensitive comparison). Does NOT mutate ENV. Handy for `vcvars env`.
|
|
55
|
+
def delta(arch: Locator.default_arch)
|
|
56
|
+
current = upcased_snapshot
|
|
57
|
+
captured = capture(arch: arch)
|
|
58
|
+
captured.reject do |k, v|
|
|
59
|
+
SKIP.include?(k.upcase) || current[k.upcase] == v
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Run vcvars in a child process and return the full captured environment
|
|
64
|
+
# as a Hash (does NOT mutate ENV). Provide an explicit vcvars path to skip
|
|
65
|
+
# location.
|
|
66
|
+
def capture(vcvars: nil, arch: Locator.default_arch)
|
|
67
|
+
vcvars ||= begin
|
|
68
|
+
inst = Locator.find(arch: arch)
|
|
69
|
+
raise Error, "Could not locate a vcvars batch script for arch #{arch.inspect}" if inst.nil?
|
|
70
|
+
inst.vcvars
|
|
71
|
+
end
|
|
72
|
+
raise Error, "vcvars script not found: #{vcvars}" unless File.exist?(vcvars)
|
|
73
|
+
|
|
74
|
+
out = run_capture_batch(vcvars)
|
|
75
|
+
parse_set_output(out)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parse the stdout of "<vcvars> ; echo MARKER ; set" into a Hash. Public so
|
|
79
|
+
# it can be unit-tested without invoking a real vcvars script.
|
|
80
|
+
def parse_set_output(out)
|
|
81
|
+
body = out.split(MARKER, 2)[1]
|
|
82
|
+
raise Error, "vcvars produced no environment (marker missing). " \
|
|
83
|
+
"Output head: #{out[0, 200].inspect}" if body.nil?
|
|
84
|
+
|
|
85
|
+
env = {}
|
|
86
|
+
body.each_line do |raw|
|
|
87
|
+
line = raw.chomp
|
|
88
|
+
i = line.index("=")
|
|
89
|
+
# Skip blank lines and Windows drive pseudo-vars ("=C:", "=ExitCode").
|
|
90
|
+
next if i.nil? || i.zero?
|
|
91
|
+
# Split on the FIRST "=" only — values legitimately contain "=".
|
|
92
|
+
env[line[0...i]] = line[(i + 1)..]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
unless env.keys.any? { |k| k.casecmp("INCLUDE").zero? }
|
|
96
|
+
raise Error, "vcvars ran but did not set INCLUDE; the MSVC environment " \
|
|
97
|
+
"was not initialized (is the C++ workload installed?)."
|
|
98
|
+
end
|
|
99
|
+
env
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Import a captured env Hash into ENV: skip transient vars, and only write
|
|
103
|
+
# values that differ (case-insensitively) from the current ENV. Returns the
|
|
104
|
+
# hash. Public for testing.
|
|
105
|
+
def import!(env)
|
|
106
|
+
current = upcased_snapshot
|
|
107
|
+
env.each do |k, v|
|
|
108
|
+
next if SKIP.include?(k.upcase)
|
|
109
|
+
ENV[k] = v if current[k.upcase] != v
|
|
110
|
+
end
|
|
111
|
+
env
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# --- internals -----------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def upcased_snapshot
|
|
117
|
+
snap = {}
|
|
118
|
+
ENV.each { |k, v| snap[k.upcase] = v }
|
|
119
|
+
snap
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Write a tiny batch that calls vcvars then dumps `set`, run it via a clean
|
|
123
|
+
# `cmd /c <file>` (a single unquoted token arg — avoids the embedded-quote
|
|
124
|
+
# escaping that breaks compound cmd lines on Windows), and return stdout.
|
|
125
|
+
def run_capture_batch(vcvars)
|
|
126
|
+
script = +"@echo off\r\n"
|
|
127
|
+
script << %{call "#{vcvars}"\r\n}
|
|
128
|
+
script << "echo #{MARKER}\r\n"
|
|
129
|
+
script << "set\r\n"
|
|
130
|
+
|
|
131
|
+
path = nil
|
|
132
|
+
begin
|
|
133
|
+
file = Tempfile.create(["vcvars_capture", ".bat"])
|
|
134
|
+
path = file.path
|
|
135
|
+
file.write(script)
|
|
136
|
+
file.close # must be closed before cmd.exe can read it on Windows
|
|
137
|
+
|
|
138
|
+
out, status = Open3.capture2("cmd.exe", "/c", path)
|
|
139
|
+
unless status.success?
|
|
140
|
+
raise Error, "vcvars batch exited #{status.exitstatus} (script: #{vcvars})"
|
|
141
|
+
end
|
|
142
|
+
out
|
|
143
|
+
ensure
|
|
144
|
+
File.unlink(path) if path && File.exist?(path)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
module Vcvars
|
|
7
|
+
# Finds a Visual Studio / Build Tools installation and the arch-specific
|
|
8
|
+
# vcvars batch script that activates the MSVC build environment.
|
|
9
|
+
#
|
|
10
|
+
# Strategy (most robust first):
|
|
11
|
+
# 1. `vswhere -find VC\Auxiliary\Build\<vcvars>.bat` — returns the script
|
|
12
|
+
# path directly. This is used as the primary method because on some VS
|
|
13
|
+
# layouts (observed on the VS "18" / 2026 line) `-property
|
|
14
|
+
# installationPath` comes back empty, while `-find` still works.
|
|
15
|
+
# 2. `vswhere -property installationPath` + the known relative path.
|
|
16
|
+
# 3. `VSINSTALLDIR` (set when already inside a developer environment).
|
|
17
|
+
# 4. Scan well-known install roots (Program Files\Microsoft Visual Studio).
|
|
18
|
+
module Locator
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
VSWHERE = File.join(
|
|
22
|
+
ENV["ProgramFiles(x86)"] || 'C:\Program Files (x86)',
|
|
23
|
+
"Microsoft Visual Studio", "Installer", "vswhere.exe"
|
|
24
|
+
).freeze
|
|
25
|
+
|
|
26
|
+
# Selects the newest STABLE install carrying the x64/x86 VC++ toolset.
|
|
27
|
+
# Prerelease/Insiders installs are only considered as a fallback (find
|
|
28
|
+
# retries with "-prerelease" appended), so a stable VS is preferred when
|
|
29
|
+
# both are present.
|
|
30
|
+
VSWHERE_SELECT = [
|
|
31
|
+
"-latest", "-products", "*",
|
|
32
|
+
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
Installation = Struct.new(:vs_path, :vcvars, :arch, :version, :name, keyword_init: true) do
|
|
36
|
+
def to_s
|
|
37
|
+
"#{name || 'Visual Studio'}#{version ? " (#{version})" : ''} at #{vs_path}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The arch this Ruby was built for, e.g. "x64-mswin64_140".
|
|
42
|
+
def default_arch
|
|
43
|
+
RbConfig::CONFIG["arch"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Map a Ruby/target arch to the matching vcvars script name. The build host
|
|
47
|
+
# is assumed to be x64 (overwhelmingly true for Ruby on Windows), so arm64
|
|
48
|
+
# and x86 targets use the x64-hosted cross scripts.
|
|
49
|
+
def vcvars_name(arch)
|
|
50
|
+
case arch.to_s.downcase
|
|
51
|
+
when "x64", "amd64", "x86_64", /\Ax64\b/, /x86_64/, /amd64/ then "vcvars64.bat"
|
|
52
|
+
when "arm64", "aarch64", /arm64/, /aarch64/ then "vcvarsamd64_arm64.bat"
|
|
53
|
+
when "x86", "i386", "i686", /\Ai\d86/, /\Ax86\b/ then "vcvars32.bat"
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError, "unsupported arch for vcvars: #{arch.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns an Installation, or nil if no suitable VS install is found.
|
|
60
|
+
# Memoized per arch within a process (the install location is stable).
|
|
61
|
+
def find(arch: default_arch)
|
|
62
|
+
@cache ||= {}
|
|
63
|
+
key = arch.to_s
|
|
64
|
+
return @cache[key] if @cache.key?(key)
|
|
65
|
+
|
|
66
|
+
@cache[key] = resolve(arch)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resolve(arch)
|
|
70
|
+
name = vcvars_name(arch)
|
|
71
|
+
|
|
72
|
+
if File.exist?(VSWHERE)
|
|
73
|
+
# Prefer a stable install; only then consider prerelease/Insiders.
|
|
74
|
+
[VSWHERE_SELECT, VSWHERE_SELECT + ["-prerelease"]].each do |select|
|
|
75
|
+
inst = resolve_via_vswhere(select, name, arch)
|
|
76
|
+
return inst if inst
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
bat = resolve_via_env(name) || scan_known_roots(name)
|
|
81
|
+
return nil unless bat
|
|
82
|
+
|
|
83
|
+
Installation.new(vs_path: win(vs_root_from(bat)), vcvars: win(bat),
|
|
84
|
+
arch: arch.to_s, version: nil, name: nil)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Resolve a complete Installation from ONE vswhere selection, so the vcvars
|
|
88
|
+
# path and the reported metadata (version/name) describe the SAME install.
|
|
89
|
+
def resolve_via_vswhere(select, name, arch)
|
|
90
|
+
path = vswhere(select + ["-property", "installationPath"]).strip
|
|
91
|
+
bat = nil
|
|
92
|
+
unless path.empty?
|
|
93
|
+
cand = File.join(path, "VC", "Auxiliary", "Build", name)
|
|
94
|
+
bat = cand if File.exist?(cand)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Some VS layouts return an empty installationPath; -find yields the path.
|
|
98
|
+
if bat.nil?
|
|
99
|
+
found = vswhere(select + ["-find", "VC\\Auxiliary\\Build\\#{name}"])
|
|
100
|
+
bat = found.lines.map(&:strip).find { |l| !l.empty? && File.exist?(l) }
|
|
101
|
+
return nil unless bat
|
|
102
|
+
path = vs_root_from(bat)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
version = first_nonempty(
|
|
106
|
+
vswhere(select + ["-property", "installationVersion"]).strip,
|
|
107
|
+
vswhere(select + ["-property", "catalog_productDisplayVersion"]).strip
|
|
108
|
+
)
|
|
109
|
+
display = vswhere(select + ["-property", "displayName"]).strip
|
|
110
|
+
|
|
111
|
+
Installation.new(
|
|
112
|
+
vs_path: win(path),
|
|
113
|
+
vcvars: win(bat),
|
|
114
|
+
arch: arch.to_s,
|
|
115
|
+
version: version,
|
|
116
|
+
name: display.empty? ? nil : display
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Normalize to native Windows separators for clean display (File.join mixes
|
|
121
|
+
# "/" into the backslash paths vswhere returns). Harmless for File.* / cmd.
|
|
122
|
+
def win(path)
|
|
123
|
+
path.nil? ? path : path.tr("/", "\\")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The vcvars*.bat under VSINSTALLDIR (set inside an active dev env), or nil.
|
|
127
|
+
def resolve_via_env(name)
|
|
128
|
+
vsdir = ENV["VSINSTALLDIR"]
|
|
129
|
+
return nil if vsdir.nil? || vsdir.empty?
|
|
130
|
+
|
|
131
|
+
cand = File.join(vsdir, "VC", "Auxiliary", "Build", name)
|
|
132
|
+
File.exist?(cand) ? cand : nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def first_nonempty(*values)
|
|
136
|
+
values.find { |v| v && !v.empty? }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def scan_known_roots(name)
|
|
140
|
+
bases = [ENV["ProgramFiles"], ENV["ProgramFiles(x86)"]].compact.uniq
|
|
141
|
+
# VS 2026 uses a numeric "18" directory; 2022/2019/2017 use the year.
|
|
142
|
+
versions = %w[18 2022 2019 2017]
|
|
143
|
+
editions = %w[Preview Enterprise Professional Community BuildTools]
|
|
144
|
+
bases.each do |pf|
|
|
145
|
+
root = File.join(pf, "Microsoft Visual Studio")
|
|
146
|
+
versions.each do |ver|
|
|
147
|
+
editions.each do |ed|
|
|
148
|
+
cand = File.join(root, ver, ed, "VC", "Auxiliary", "Build", name)
|
|
149
|
+
return cand if File.exist?(cand)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The install root is four levels up from <root>\VC\Auxiliary\Build\x.bat.
|
|
157
|
+
def vs_root_from(bat)
|
|
158
|
+
File.dirname(File.dirname(File.dirname(File.dirname(bat))))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Run vswhere with the given args; returns stdout ("" on any failure).
|
|
162
|
+
# Args are passed as a real argv (no shell), so the literal "*" in
|
|
163
|
+
# "-products *" reaches vswhere instead of being glob-expanded.
|
|
164
|
+
def vswhere(args)
|
|
165
|
+
out, status = Open3.capture2(VSWHERE, *args)
|
|
166
|
+
status.success? ? out : ""
|
|
167
|
+
rescue StandardError
|
|
168
|
+
""
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
data/lib/vcvars/rake.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "vcvars"
|
|
4
|
+
|
|
5
|
+
# Requiring this file auto-activates the MSVC build environment, so that
|
|
6
|
+
# rake-compiler's `rake compile` (which hardcodes `nmake` and assumes cl.exe is
|
|
7
|
+
# already on PATH) works without first opening a "Developer Command Prompt".
|
|
8
|
+
#
|
|
9
|
+
# Usage — put this BEFORE Rake::ExtensionTask.new in your Rakefile:
|
|
10
|
+
#
|
|
11
|
+
# require "vcvars/rake"
|
|
12
|
+
# require "rake/extensiontask"
|
|
13
|
+
# Rake::ExtensionTask.new("my_ext", spec) { |e| e.lib_dir = "lib/my_ext" }
|
|
14
|
+
#
|
|
15
|
+
# It is a no-op off mswin Ruby, and a no-op inside an already-active developer
|
|
16
|
+
# environment. rake-compiler shells out with FileUtils#sh, which inherits this
|
|
17
|
+
# process's ENV, so activating once at Rakefile load time covers the extconf,
|
|
18
|
+
# nmake build, and nmake install steps.
|
|
19
|
+
if Vcvars.mswin?
|
|
20
|
+
begin
|
|
21
|
+
Vcvars.activate!
|
|
22
|
+
rescue Vcvars::Error => e
|
|
23
|
+
warn "[vcvars] could not load the MSVC environment: #{e.message}"
|
|
24
|
+
warn "[vcvars] run `vcvars doctor` to diagnose."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module Vcvars
|
|
29
|
+
# Optional belt-and-suspenders: call AFTER defining your compile task to also
|
|
30
|
+
# guarantee activation as a task dependency (covers sub-rake invocations that
|
|
31
|
+
# bypass the load-time activation above).
|
|
32
|
+
#
|
|
33
|
+
# Rake::ExtensionTask.new("my_ext", spec)
|
|
34
|
+
# Vcvars.enhance_compile_task!
|
|
35
|
+
def self.enhance_compile_task!(task_name = "compile")
|
|
36
|
+
return unless mswin?
|
|
37
|
+
return unless defined?(Rake) && Rake::Task.task_defined?(task_name)
|
|
38
|
+
|
|
39
|
+
Rake::Task.define_task("vcvars:activate") { Vcvars.activate! }
|
|
40
|
+
Rake::Task[task_name].enhance(["vcvars:activate"])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "vcvars/version"
|
|
5
|
+
|
|
6
|
+
module Vcvars
|
|
7
|
+
# Generates a new C-extension gem skeleton that is correct for an MSVC
|
|
8
|
+
# (mswin) Ruby: an MSVC-aware extconf.rb, a Rakefile wired to `vcvars/rake`
|
|
9
|
+
# (so `rake compile` works without a Developer Command Prompt), a
|
|
10
|
+
# warning-clean sample .c, and a passing test.
|
|
11
|
+
class Scaffold
|
|
12
|
+
NAME_RE = /\A[a-z][a-z0-9_]*\z/
|
|
13
|
+
|
|
14
|
+
attr_reader :name, :module_name, :dest
|
|
15
|
+
|
|
16
|
+
def initialize(name, dir: nil)
|
|
17
|
+
@name = name.to_s
|
|
18
|
+
unless @name =~ NAME_RE
|
|
19
|
+
raise Error, "invalid extension name #{name.inspect}: use lowercase " \
|
|
20
|
+
"letters, digits and underscores, starting with a letter (e.g. my_ext)."
|
|
21
|
+
end
|
|
22
|
+
@module_name = camelize(@name)
|
|
23
|
+
@dest = File.expand_path(dir || @name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Writes the skeleton. Returns the list of created file paths.
|
|
27
|
+
# Raises if the destination already contains files (unless force: true).
|
|
28
|
+
def generate(force: false)
|
|
29
|
+
if File.directory?(@dest) && !Dir.empty?(@dest) && !force
|
|
30
|
+
raise Error, "destination #{@dest} already exists and is not empty " \
|
|
31
|
+
"(pass force: true / --force to overwrite)."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
created = []
|
|
35
|
+
files.each do |relpath, content|
|
|
36
|
+
full = File.join(@dest, relpath)
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(full))
|
|
38
|
+
File.write(full, content)
|
|
39
|
+
created << full
|
|
40
|
+
end
|
|
41
|
+
created
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# relpath => contents
|
|
45
|
+
def files
|
|
46
|
+
{
|
|
47
|
+
"#{name}.gemspec" => render(GEMSPEC),
|
|
48
|
+
"Gemfile" => render(GEMFILE),
|
|
49
|
+
"Rakefile" => render(RAKEFILE),
|
|
50
|
+
"README.md" => render(README),
|
|
51
|
+
"LICENSE.txt" => render(LICENSE),
|
|
52
|
+
".gitignore" => render(GITIGNORE),
|
|
53
|
+
"lib/#{name}.rb" => render(LIB_ENTRY),
|
|
54
|
+
"lib/#{name}/version.rb" => render(LIB_VERSION),
|
|
55
|
+
"ext/#{name}/extconf.rb" => render(EXTCONF),
|
|
56
|
+
"ext/#{name}/#{name}.c" => render(EXT_C),
|
|
57
|
+
"test/test_#{name}.rb" => render(TEST)
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def camelize(str)
|
|
64
|
+
str.split("_").map { |p| p.empty? ? p : (p[0].upcase + p[1..]) }.join
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render(template)
|
|
68
|
+
template
|
|
69
|
+
.gsub("{{name}}", name)
|
|
70
|
+
.gsub("{{Name}}", module_name)
|
|
71
|
+
.gsub("{{vcvars_version}}", Vcvars::VERSION)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# --- templates -----------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
GEMSPEC = <<~'RUBY'
|
|
77
|
+
# frozen_string_literal: true
|
|
78
|
+
|
|
79
|
+
require_relative "lib/{{name}}/version"
|
|
80
|
+
|
|
81
|
+
Gem::Specification.new do |spec|
|
|
82
|
+
spec.name = "{{name}}"
|
|
83
|
+
spec.version = {{Name}}::VERSION
|
|
84
|
+
spec.authors = ["Your Name"]
|
|
85
|
+
spec.email = ["you@example.com"]
|
|
86
|
+
spec.summary = "A native C extension for Ruby."
|
|
87
|
+
# NOTE: RubyGems rejects a description that begins with TODO/FIXME, so
|
|
88
|
+
# this ships as a valid placeholder — edit it, don't prefix it with TODO.
|
|
89
|
+
spec.description = "The {{name}} native C extension for Ruby, scaffolded by vcvars."
|
|
90
|
+
spec.homepage = "https://example.com/{{name}}"
|
|
91
|
+
spec.license = "MIT"
|
|
92
|
+
spec.required_ruby_version = ">= 3.0"
|
|
93
|
+
|
|
94
|
+
spec.files = Dir.glob(["lib/**/*.rb", "ext/**/*.{c,h,rb}", "README.md", "LICENSE*"])
|
|
95
|
+
spec.require_paths = ["lib"]
|
|
96
|
+
|
|
97
|
+
# Build the extension from source on `gem install`.
|
|
98
|
+
spec.extensions = ["ext/{{name}}/extconf.rb"]
|
|
99
|
+
|
|
100
|
+
spec.add_development_dependency "rake"
|
|
101
|
+
spec.add_development_dependency "rake-compiler"
|
|
102
|
+
spec.add_development_dependency "minitest"
|
|
103
|
+
# vcvars loads the MSVC toolchain so `rake compile` works on an mswin Ruby.
|
|
104
|
+
spec.add_development_dependency "vcvars", ">= {{vcvars_version}}"
|
|
105
|
+
end
|
|
106
|
+
RUBY
|
|
107
|
+
|
|
108
|
+
GEMFILE = <<~'RUBY'
|
|
109
|
+
# frozen_string_literal: true
|
|
110
|
+
|
|
111
|
+
source "https://rubygems.org"
|
|
112
|
+
|
|
113
|
+
gemspec
|
|
114
|
+
RUBY
|
|
115
|
+
|
|
116
|
+
RAKEFILE = <<~'RUBY'
|
|
117
|
+
# frozen_string_literal: true
|
|
118
|
+
|
|
119
|
+
require "vcvars/rake" # loads the MSVC build env on mswin Ruby (no-op elsewhere)
|
|
120
|
+
require "rake/extensiontask"
|
|
121
|
+
require "rake/testtask"
|
|
122
|
+
|
|
123
|
+
spec = Gem::Specification.load("{{name}}.gemspec")
|
|
124
|
+
|
|
125
|
+
Rake::ExtensionTask.new("{{name}}", spec) do |ext|
|
|
126
|
+
ext.lib_dir = "lib/{{name}}" # built {{name}}.so lands at lib/{{name}}/{{name}}.so
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
Rake::TestTask.new(test: :compile) do |t|
|
|
130
|
+
t.libs << "test" << "lib"
|
|
131
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
|
132
|
+
t.warning = false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
task default: :test
|
|
136
|
+
RUBY
|
|
137
|
+
|
|
138
|
+
LIB_ENTRY = <<~'RUBY'
|
|
139
|
+
# frozen_string_literal: true
|
|
140
|
+
|
|
141
|
+
require "{{name}}/version"
|
|
142
|
+
require "{{name}}/{{name}}" # the compiled C extension (lib/{{name}}/{{name}}.so)
|
|
143
|
+
|
|
144
|
+
module {{Name}}
|
|
145
|
+
end
|
|
146
|
+
RUBY
|
|
147
|
+
|
|
148
|
+
LIB_VERSION = <<~'RUBY'
|
|
149
|
+
# frozen_string_literal: true
|
|
150
|
+
|
|
151
|
+
module {{Name}}
|
|
152
|
+
VERSION = "0.1.0"
|
|
153
|
+
end
|
|
154
|
+
RUBY
|
|
155
|
+
|
|
156
|
+
EXTCONF = <<~'RUBY'
|
|
157
|
+
# frozen_string_literal: true
|
|
158
|
+
#
|
|
159
|
+
# extconf.rb for the {{name}} C extension.
|
|
160
|
+
#
|
|
161
|
+
# mkmf reads RbConfig and emits the right Makefile per platform. On an
|
|
162
|
+
# MSVC (mswin) Ruby it generates an *nmake* Makefile whose compile line is
|
|
163
|
+
# "cl -nologo $(INCFLAGS) -MD ... -Fo$(@) -c -Tc<src>". The -MD (dynamic
|
|
164
|
+
# CRT) flag is baked into RbConfig, so every object links against the same
|
|
165
|
+
# CRT as Ruby. NEVER force -MT/-MTd: a static-CRT extension mixed with
|
|
166
|
+
# Ruby's dynamic CRT causes LNK2005 duplicate symbols and heap corruption.
|
|
167
|
+
#
|
|
168
|
+
# The Ruby import library (e.g. x64-vcruntime140-ruby340.lib) is added to
|
|
169
|
+
# the link line automatically — you do not name or locate it here.
|
|
170
|
+
|
|
171
|
+
require "mkmf"
|
|
172
|
+
|
|
173
|
+
# Optional: wrap an external library (adds --with-foo-dir/-include/-lib,
|
|
174
|
+
# and is mswin-aware: -I to $INCFLAGS, -libpath: to $LIBPATH).
|
|
175
|
+
# dir_config("foo")
|
|
176
|
+
# have_header("foo.h") or abort "missing foo.h"
|
|
177
|
+
# have_library("foo", "foo_init") or abort "missing foo.lib" # bare name; no -l on mswin
|
|
178
|
+
|
|
179
|
+
# append_cflags probes each flag with warnings-as-errors; cl rejects
|
|
180
|
+
# GCC-style flags like -std=gnu11, so this is a safe no-op on MSVC and only
|
|
181
|
+
# applies on gcc/clang (MinGW/UCRT/Unix).
|
|
182
|
+
append_cflags(["-std=c11"])
|
|
183
|
+
|
|
184
|
+
# "{{name}}/{{name}}" installs the .so under {{name}}/ so it can be required
|
|
185
|
+
# as "{{name}}/{{name}}". On mswin the artifact is {{name}}.so (DLEXT is "so").
|
|
186
|
+
create_makefile("{{name}}/{{name}}")
|
|
187
|
+
RUBY
|
|
188
|
+
|
|
189
|
+
EXT_C = <<~'C'
|
|
190
|
+
#include <ruby.h>
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
* On mswin Ruby the Init_ entry point is exported automatically by mkmf via
|
|
194
|
+
* an auto-generated .def file passed to the linker — do NOT add
|
|
195
|
+
* __declspec(dllexport)/RUBY_FUNC_EXPORTED here.
|
|
196
|
+
*
|
|
197
|
+
* Ruby's CFLAGS promote several warnings to errors (-we4028 -we4047 -we4013
|
|
198
|
+
* -we4142). Including <ruby.h>, using correct VALUE signatures, the
|
|
199
|
+
* RUBY_METHOD_FUNC cast, and the NUM2INT/INT2NUM + rb_str_* APIs keeps the
|
|
200
|
+
* build warning- and error-clean. Unused params (C4100) are suppressed by
|
|
201
|
+
* Ruby's own flags, so an unused `self` is fine.
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
/* Adds two integers and returns the sum. */
|
|
205
|
+
static VALUE
|
|
206
|
+
{{name}}_add(VALUE self, VALUE a, VALUE b)
|
|
207
|
+
{
|
|
208
|
+
int x = NUM2INT(a);
|
|
209
|
+
int y = NUM2INT(b);
|
|
210
|
+
return INT2NUM(x + y);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Returns a greeting string for the given name. */
|
|
214
|
+
static VALUE
|
|
215
|
+
{{name}}_greet(VALUE self, VALUE name)
|
|
216
|
+
{
|
|
217
|
+
VALUE who = StringValue(name); /* validate/coerce to String */
|
|
218
|
+
VALUE out = rb_str_new_cstr("Hello, ");
|
|
219
|
+
rb_str_append(out, who);
|
|
220
|
+
rb_str_cat_cstr(out, "!");
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
void
|
|
225
|
+
Init_{{name}}(void)
|
|
226
|
+
{
|
|
227
|
+
VALUE m{{Name}} = rb_define_module("{{Name}}");
|
|
228
|
+
rb_define_singleton_method(m{{Name}}, "add", RUBY_METHOD_FUNC({{name}}_add), 2);
|
|
229
|
+
rb_define_singleton_method(m{{Name}}, "greet", RUBY_METHOD_FUNC({{name}}_greet), 1);
|
|
230
|
+
}
|
|
231
|
+
C
|
|
232
|
+
|
|
233
|
+
TEST = <<~'RUBY'
|
|
234
|
+
# frozen_string_literal: true
|
|
235
|
+
|
|
236
|
+
require "minitest/autorun"
|
|
237
|
+
require "{{name}}"
|
|
238
|
+
|
|
239
|
+
class {{Name}}Test < Minitest::Test
|
|
240
|
+
def test_add
|
|
241
|
+
assert_equal 5, {{Name}}.add(2, 3)
|
|
242
|
+
assert_equal(-1, {{Name}}.add(2, -3))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def test_greet
|
|
246
|
+
assert_equal "Hello, Ruby!", {{Name}}.greet("Ruby")
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
RUBY
|
|
250
|
+
|
|
251
|
+
README = <<~'MARKDOWN'
|
|
252
|
+
# {{name}}
|
|
253
|
+
|
|
254
|
+
A native C extension for Ruby, scaffolded by [vcvars](https://rubygems.org/gems/vcvars).
|
|
255
|
+
|
|
256
|
+
## Build & test
|
|
257
|
+
|
|
258
|
+
On an MSVC (mswin) Ruby you do **not** need to open a Developer Command
|
|
259
|
+
Prompt — the `Rakefile` requires `vcvars/rake`, which locates Visual Studio
|
|
260
|
+
and loads the MSVC toolchain automatically.
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
bundle install
|
|
264
|
+
rake compile # builds ext/{{name}}/{{name}}.c -> lib/{{name}}/{{name}}.so
|
|
265
|
+
rake test
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
If a build fails, run `vcvars doctor` to diagnose the toolchain.
|
|
269
|
+
|
|
270
|
+
## Usage
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
require "{{name}}"
|
|
274
|
+
|
|
275
|
+
{{Name}}.add(2, 3) # => 5
|
|
276
|
+
{{Name}}.greet("Ruby") # => "Hello, Ruby!"
|
|
277
|
+
```
|
|
278
|
+
MARKDOWN
|
|
279
|
+
|
|
280
|
+
LICENSE = <<~'TEXT'
|
|
281
|
+
MIT License
|
|
282
|
+
|
|
283
|
+
Copyright (c) 2026 Your Name
|
|
284
|
+
|
|
285
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
286
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
287
|
+
in the Software without restriction, including without limitation the rights
|
|
288
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
289
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
290
|
+
furnished to do so, subject to the following conditions:
|
|
291
|
+
|
|
292
|
+
The above copyright notice and this permission notice shall be included in all
|
|
293
|
+
copies or substantial portions of the Software.
|
|
294
|
+
|
|
295
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
296
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
297
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
298
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
299
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
300
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
301
|
+
SOFTWARE.
|
|
302
|
+
TEXT
|
|
303
|
+
|
|
304
|
+
GITIGNORE = <<~'TEXT'
|
|
305
|
+
/pkg/
|
|
306
|
+
/tmp/
|
|
307
|
+
/.bundle/
|
|
308
|
+
Gemfile.lock
|
|
309
|
+
|
|
310
|
+
# build artifacts
|
|
311
|
+
*.o
|
|
312
|
+
*.obj
|
|
313
|
+
*.so
|
|
314
|
+
*.dll
|
|
315
|
+
*.lib
|
|
316
|
+
*.exp
|
|
317
|
+
*.pdb
|
|
318
|
+
*.def
|
|
319
|
+
Makefile
|
|
320
|
+
mkmf.log
|
|
321
|
+
/lib/{{name}}/{{name}}.so
|
|
322
|
+
TEXT
|
|
323
|
+
end
|
|
324
|
+
end
|