Module: Vcdeps::Doctor

Defined in:
lib/vcdeps/doctor.rb

Overview

Diagnoses vcpkg acquisition + mkmf wiring, mirroring Vcvars::Doctor's Check/icon/Summary pattern. Delegates the toolchain checks to vcvars (a hard dependency whose Doctor is public API) and adds the vcdeps-specific checks (§2.9): resolution, manifest baseline, opt-dir shadowing, Ruby-bin DLL collisions, triplet sanity, binary cache, offline readiness, home hygiene.

Defined Under Namespace

Classes: Check

Class Method Summary collapse

Class Method Details

.actions_root_checksObject

  1. VCPKG_INSTALLATION_ROOT set but invalid (issue #9269).


89
90
91
92
93
94
95
96
97
98
# File 'lib/vcdeps/doctor.rb', line 89

def actions_root_checks
  root = ENV["VCPKG_INSTALLATION_ROOT"]
  return [] if root.nil? || root.empty?
  return [] if ToolFinder.valid_root(root)

  [Check.new(status: :warn, label: "VCPKG_INSTALLATION_ROOT is set but invalid",
             detail: "#{root.tr('/', '\\')} lacks vcpkg.exe/.vcpkg-root (the " \
                     "runner-images #9269 bug). vcdeps SKIPS it; nothing to fix " \
                     "unless you rely on it.")]
end

.cache_checksObject

  1. Binary cache: %LOCALAPPDATA%\vcpkg\archives (or override) + entry count.


167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/vcdeps/doctor.rb', line 167

def cache_checks
  cache = ENV["VCPKG_DEFAULT_BINARY_CACHE"]
  cache = File.join(ENV["LOCALAPPDATA"] || "", "vcpkg", "archives") if cache.nil? || cache.empty?
  if File.directory?(cache)
    count = Dir[File.join(cache.tr("\\", "/"), "**", "*.zip")].size
    [Check.new(status: :info, label: "Binary cache present (#{count} archive(s))",
               detail: cache.tr("/", "\\"))]
  else
    [Check.new(status: :info, label: "Binary cache not yet created",
               detail: "#{cache.tr('/', '\\')} will hold built ports; first cold " \
                       "build populates it.")]
  end
end

.collision_checks(manifest) ⇒ Object

  1. Ruby-bin DLL collisions: scan the install's/vendored DLL base names against Ruby's bindir.


133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/vcdeps/doctor.rb', line 133

def collision_checks(manifest)
  return [] if manifest.nil?

  bindir = RbConfig::CONFIG["bindir"].to_s
  return [] if bindir.empty?

  names = manifest_dll_basenames(manifest)
  collisions = names.select { |n| File.exist?(File.join(bindir, n)) }
  return [] if collisions.empty?

  collisions.map do |n|
    Check.new(status: :warn, label: "DLL name collides with Ruby's bin: #{n}",
              detail: "#{File.join(bindir, n).tr('/', '\\')} exists; first-loaded " \
                      "wins process-wide (R§9.2). The preload shim runs first " \
                      "under normal require order.")
  end
end

.healthy?(checks) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
# File 'lib/vcdeps/doctor.rb', line 40

def healthy?(checks)
  checks.none? { |c| c.status == :fail }
end

.home_checks(deep:) ⇒ Object

  1. Vcdeps.home hygiene: spaces / non-ASCII WARN; size INFO on deep.


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/vcdeps/doctor.rb', line 195

def home_checks(deep:)
  out = []
  h = Vcdeps.home
  if h =~ /\s/
    out << Check.new(status: :warn, label: "VCDEPS_HOME path contains spaces",
                     detail: "#{h} — naked -I flags and some port builds mishandle " \
                             "spaces. Set VCDEPS_HOME to a short ASCII path.")
  elsif h =~ /[^\x00-\x7F]/
    out << Check.new(status: :warn, label: "VCDEPS_HOME path has non-ASCII characters",
                     detail: "#{h} — some port build scripts mishandle it. Set " \
                             "VCDEPS_HOME to a short ASCII path.")
  else
    out << Check.new(status: :ok, label: "vcdeps home path is clean", detail: h)
  end

  if deep && File.directory?(h)
    out << Check.new(status: :info, label: "vcdeps home size: #{home_size_mb(h)} MB",
                     detail: h)
  end
  out
end

.home_size_mb(dir) ⇒ Object



253
254
255
256
257
258
259
260
261
# File 'lib/vcdeps/doctor.rb', line 253

def home_size_mb(dir)
  total = 0
  Dir.glob(File.join(dir.tr("\\", "/"), "**", "*"), File::FNM_DOTMATCH) do |f|
    total += File.size(f) if File.file?(f)
  end
  (total / (1024.0 * 1024.0)).round(1)
rescue StandardError
  0
end

.manifest_checks(manifest) ⇒ Object

  1. Manifest (when given): parses; builtin-baseline present or vcpkg-configuration.json beside it — else FAIL with the baseline remedy.


102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/vcdeps/doctor.rb', line 102

def manifest_checks(manifest)
  return [] if manifest.nil?

  dir = File.expand_path(manifest)
  path = File.join(dir, Manifest::MANIFEST_NAME)
  return [Check.new(status: :fail, label: "No manifest at #{path.tr('/', '\\')}",
                    detail: "Create a vcpkg.json there.")] unless File.exist?(path)

  begin
    Manifest.load!(dir)
    [Check.new(status: :ok, label: "Manifest is valid and baselined",
               detail: path.tr("/", "\\"))]
  rescue ManifestError => e
    [Check.new(status: :fail, label: "Manifest problem", detail: e.message)]
  end
end

.manifest_dll_basenames(manifest) ⇒ Object

Best-effort: the DLL base names a manifest would vendor, read from an already-installed tree if present (so doctor stays offline and cheap).



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/vcdeps/doctor.rb', line 233

def manifest_dll_basenames(manifest)
  dir = File.expand_path(manifest)
  tool = safe_tool
  return [] unless tool

  triplet = ENV["VCDEPS_TRIPLET"]
  triplet = (Triplet.default rescue nil) if triplet.nil? || triplet.empty?
  return [] unless triplet

  key = Manifest.key(dir, triplet, tool.version)
  root = File.join(Vcdeps.home, "installed", key)
  return [] unless File.directory?(root)

  inst = Installed.new(root: root, triplet: triplet, manifest_dir: dir,
                       key: key, tool: tool)
  inst.dlls.map { |d| File.basename(d) }
rescue StandardError
  []
end

.offline_checksObject

  1. Registry/offline readiness: baseline tree under registries.


182
183
184
185
186
187
188
189
190
191
192
# File 'lib/vcdeps/doctor.rb', line 182

def offline_checks
  reg = File.join(ENV["LOCALAPPDATA"] || "", "vcpkg", "registries")
  if File.directory?(reg) && !Dir.empty?(reg.tr("\\", "/"))
    [Check.new(status: :info, label: "Registry cache present (offline-capable)",
               detail: reg.tr("/", "\\"))]
  else
    [Check.new(status: :info, label: "Registry cache empty",
               detail: "First install of a baseline needs network for the registry " \
                       "git-tree fetch.")]
  end
end

.optdir_checksObject

  1. opt-dir shadowing: parse configure_args for --with-opt-dir.


120
121
122
123
124
125
126
127
128
129
# File 'lib/vcdeps/doctor.rb', line 120

def optdir_checks
  args = RbConfig::CONFIG["configure_args"].to_s
  m = args.match(/--with-opt-dir=(\S+)/)
  return [] unless m

  [Check.new(status: :warn, label: "Ruby was built with --with-opt-dir",
             detail: "#{m[1]} is folded into EVERY extension's paths AHEAD of " \
                     "appended dirs (opt-dir shadowing, R§8.4). vcdeps PREPENDS " \
                     "its -I / -libpath so its vcpkg tree wins; no action needed.")]
end

.run(manifest: nil, deep: true) ⇒ Object

Returns an Array. manifest (a dir) enables the manifest/triplet checks; deep: true adds the home-size INFO.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/vcdeps/doctor.rb', line 25

def run(manifest: nil, deep: true)
  checks = []
  checks.concat(toolchain_checks)
  checks.concat(tool_checks)
  checks.concat(actions_root_checks)
  checks.concat(manifest_checks(manifest))
  checks.concat(optdir_checks)
  checks.concat(collision_checks(manifest))
  checks.concat(triplet_checks)
  checks.concat(cache_checks)
  checks.concat(offline_checks)
  checks.concat(home_checks(deep: deep))
  checks
end

.safe_locateObject

--- internals -----------------------------------------------------------



219
220
221
222
223
# File 'lib/vcdeps/doctor.rb', line 219

def safe_locate
  Vcvars.locate
rescue StandardError
  nil
end

.safe_toolObject



225
226
227
228
229
# File 'lib/vcdeps/doctor.rb', line 225

def safe_tool
  Vcdeps.tool
rescue StandardError
  nil
end

.tool_checksObject

  1. vcpkg resolution: source/path/version, or FAIL + the three remedies.


74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/vcdeps/doctor.rb', line 74

def tool_checks
  tool = safe_tool
  if tool
    [Check.new(status: :ok, label: "vcpkg resolved (#{tool.source})",
               detail: "#{tool.exe}\n       version #{tool.version}")]
  else
    [Check.new(status: :fail, label: "No usable vcpkg found",
               detail: "Remedies: (a) install the VS component " \
                       "Microsoft.VisualStudio.Component.Vcpkg; (b) set " \
                       "VCPKG_ROOT to an existing instance; (c) run " \
                       "`vcdeps bootstrap` (or VCDEPS_BOOTSTRAP=1).")]
  end
end

.toolchain_checksObject

  1. mswin Ruby + CRT linkage (delegates to vcvars). 2. vcvars viability.


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/vcdeps/doctor.rb', line 45

def toolchain_checks
  out = []
  out << if Vcvars::Doctor.mswin?
    Check.new(status: :ok, label: "Ruby is a native MSVC (mswin) build",
              detail: "#{RUBY_VERSION} #{RbConfig::CONFIG['arch']}, CRT " \
                      "#{Vcvars::Doctor.crt_flag}")
  else
    Check.new(status: :fail, label: "Ruby is NOT an mswin build",
              detail: "vcdeps targets MSVC (mswin) Ruby only. Your Ruby is " \
                      "#{RbConfig::CONFIG['arch']}. Not supported on MinGW/UCRT.")
  end

  inst = safe_locate
  out << if inst
    Check.new(status: :ok, label: "Visual Studio located", detail: inst.to_s)
  else
    Check.new(status: :warn, label: "No Visual Studio located via vcvars",
              detail: "vcdeps needs cl.exe to build extensions and to find the " \
                      "VS-bundled vcpkg. Install \"Desktop development with " \
                      "C++\"; run `vcvars doctor`.")
  end
  out << Check.new(status: (Vcvars.active? ? :ok : :info),
                   label: "Developer environment #{Vcvars.active? ? 'ACTIVE' : 'not yet active'}",
                   detail: Vcvars.active? ? nil : "vcdeps activates it on demand " \
                           "(Vcvars.activate!); harmless if inactive now.")
  out
end

.triplet_checksObject

  1. Triplet sanity: resolved triplet vs §2.6 rules.


152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/vcdeps/doctor.rb', line 152

def triplet_checks
  triplet = ENV["VCDEPS_TRIPLET"]
  triplet = (Triplet.default rescue nil) if triplet.nil? || triplet.empty?
  return [Check.new(status: :warn, label: "Cannot derive a triplet for this arch",
                    detail: "Set VCDEPS_TRIPLET.")] if triplet.nil?

  begin
    Triplet.validate!(triplet)
    [Check.new(status: :ok, label: "Triplet OK: #{triplet}", detail: nil)]
  rescue TripletError => e
    [Check.new(status: :fail, label: "Triplet rejected", detail: e.message)]
  end
end