vcdeps 0.1.0 → 0.1.1

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.
@@ -1,163 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "open3"
4
- require "fileutils"
5
- require "net/http"
6
- require "uri"
7
- require "vcdeps/tool"
8
-
9
- module Vcdeps
10
- # Creates a private, registration-free vcpkg instance under <home>\vcpkg using
11
- # the same mechanism as Microsoft's vcpkg-init (R§2.4): download vcpkg.exe from
12
- # the vcpkg-tool GitHub release, then run `vcpkg.exe bootstrap-standalone` with
13
- # VCPKG_ROOT=<home>\vcpkg in the child env. Never elevates, never touches the
14
- # registry, never writes outside home.
15
- module Bootstrap
16
- module_function
17
-
18
- OPEN_TIMEOUT = 30
19
- READ_TIMEOUT = 30
20
- MAX_REDIRECTS = 5
21
-
22
- # Idempotent: an already-valid private root returns immediately. Progress
23
- # lines to `out`. Raises Vcdeps::BootstrapError on any failure.
24
- def run!(out: $stderr)
25
- root = File.join(Vcdeps.home, "vcpkg").tr("/", "\\")
26
-
27
- if (exe = ToolFinder.valid_root(root))
28
- return ToolFinder.build_tool(exe, :private)
29
- end
30
-
31
- FileUtils.mkdir_p(root)
32
- exe_path = File.join(root, "vcpkg.exe")
33
-
34
- out&.puts("[vcdeps] downloading vcpkg.exe (~7 MB) from " \
35
- "#{ToolFinder::BOOTSTRAP_URL} ...")
36
- download!(ToolFinder::BOOTSTRAP_URL, exe_path)
37
-
38
- out&.puts("[vcdeps] running `vcpkg bootstrap-standalone` in #{root} ...")
39
- bootstrap_standalone!(exe_path, root, out)
40
-
41
- resolved = ToolFinder.valid_root(root)
42
- unless resolved
43
- raise BootstrapError, "vcdeps: bootstrap-standalone completed but " \
44
- "#{root} is not a valid vcpkg root (.vcpkg-root missing). Run " \
45
- "`vcdeps doctor`."
46
- end
47
-
48
- ToolFinder.build_tool(resolved, :private)
49
- end
50
-
51
- # Net::HTTP GET to `dest`, following up to MAX_REDIRECTS redirects, with
52
- # TLS and bounded timeouts. Writes atomically (temp then rename).
53
- def download!(url, dest, redirects_left = MAX_REDIRECTS)
54
- uri = URI.parse(url)
55
- http = Net::HTTP.new(uri.host, uri.port)
56
- http.use_ssl = (uri.scheme == "https")
57
- http.open_timeout = OPEN_TIMEOUT
58
- http.read_timeout = READ_TIMEOUT
59
-
60
- tmp = "#{dest}.tmp.#{Process.pid}"
61
- http.start do |conn|
62
- request = Net::HTTP::Get.new(uri)
63
- conn.request(request) do |response|
64
- case response
65
- when Net::HTTPSuccess
66
- File.open(tmp, "wb") do |io|
67
- response.read_body { |chunk| io.write(chunk) }
68
- end
69
- when Net::HTTPRedirection
70
- raise BootstrapError, "vcdeps: too many redirects fetching " \
71
- "#{url}" if redirects_left <= 0
72
-
73
- location = response["location"]
74
- next_url = absolutize(location, uri)
75
- # The bootstrap artifact is executed, so it MUST arrive over TLS
76
- # end-to-end. GitHub release downloads normally redirect (to
77
- # objects.githubusercontent.com); honoring a `Location: http://...`
78
- # would silently drop TLS and let an on-path attacker substitute the
79
- # executable. Refuse any non-HTTPS redirect target.
80
- unless URI.parse(next_url).scheme == "https"
81
- raise BootstrapError, "vcdeps: refusing non-HTTPS redirect to " \
82
- "#{next_url} while fetching vcpkg.exe (TLS downgrade)"
83
- end
84
- return download!(next_url, dest, redirects_left - 1)
85
- else
86
- raise BootstrapError, "vcdeps: download of #{url} failed: " \
87
- "#{response.code} #{response.message}"
88
- end
89
- end
90
- end
91
-
92
- File.rename(tmp, dest)
93
- rescue BootstrapError
94
- cleanup_tmp(tmp)
95
- raise
96
- rescue StandardError => e
97
- cleanup_tmp(tmp)
98
- raise BootstrapError, "vcdeps: download of #{url} failed: #{e.class}: " \
99
- "#{e.message}"
100
- end
101
-
102
- def bootstrap_standalone!(exe, root, out)
103
- env = { "VCPKG_ROOT" => root }
104
- env["VCPKG_DISABLE_METRICS"] = "1" unless ENV["VCDEPS_METRICS"] == "1"
105
-
106
- combined = +""
107
- Open3.popen2e(env, exe, "bootstrap-standalone") do |_stdin, oe, wt|
108
- pid = wt.pid
109
- oe.binmode
110
- begin
111
- oe.each_line do |raw|
112
- line = raw.dup.force_encoding("UTF-8").scrub("�")
113
- combined << line
114
- out&.print(line)
115
- end
116
- status = wt.value
117
- unless status.success?
118
- raise BootstrapError, "vcdeps: `vcpkg bootstrap-standalone` exited " \
119
- "#{status.exitstatus}. Output tail: " \
120
- "#{combined[-500..] || combined}"
121
- end
122
- rescue BootstrapError
123
- raise
124
- rescue Exception # rubocop:disable Lint/RescueException
125
- # ANY abnormal unwind (Interrupt, Timeout, a raise from the out block):
126
- # kill THIS child before Open3's block teardown runs `wait_thr.join`,
127
- # which would otherwise BLOCK until the orphaned bootstrap child (busy
128
- # downloading/extracting/cloning) exits on its own. Same discipline as
129
- # Runner.run — vcpkg.exe never outlives the call (§3.2).
130
- kill_and_close(pid, oe)
131
- raise
132
- end
133
- end
134
- end
135
-
136
- # Shared abnormal-unwind teardown for the two child-spawn sites (here and
137
- # Runner.run share the same shape so neither can drift): TerminateProcess the
138
- # child, then close the pipe so Open3's drain returns at once.
139
- def kill_and_close(pid, io)
140
- begin
141
- Process.kill(:KILL, pid)
142
- rescue Errno::ESRCH, RangeError
143
- nil
144
- end
145
- begin
146
- io.close
147
- rescue IOError
148
- nil
149
- end
150
- end
151
-
152
- def absolutize(location, base)
153
- u = URI.parse(location)
154
- u.absolute? ? location : (base + location).to_s
155
- end
156
-
157
- def cleanup_tmp(tmp)
158
- File.unlink(tmp) if tmp && File.exist?(tmp)
159
- rescue StandardError
160
- nil
161
- end
162
- end
163
- end
data/lib/vcdeps/cli.rb DELETED
@@ -1,206 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "vcdeps"
4
- require "vcvars"
5
-
6
- module Vcdeps
7
- # Command-line interface for the `vcdeps` executable. Mirrors the vcvars CLI:
8
- # `run` returns an Integer exit status (never calls exit), dispatch is a plain
9
- # case, flags are hand-parsed (no optparse), and one rescue turns every known
10
- # error into a clean one-liner with no backtrace.
11
- class CLI
12
- def self.start(argv)
13
- new.run(argv)
14
- end
15
-
16
- # Returns a process exit status (Integer).
17
- def run(argv)
18
- argv = argv.dup
19
- command = argv.shift
20
-
21
- case command
22
- when "doctor" then cmd_doctor(argv)
23
- when "where" then cmd_where(argv)
24
- when "install" then cmd_install(argv)
25
- when "vendor" then cmd_vendor(argv)
26
- when "baseline" then cmd_baseline(argv)
27
- when "bootstrap" then cmd_bootstrap(argv)
28
- when "version", "-v", "--version" then cmd_version
29
- when nil, "help", "-h", "--help" then print_help; 0
30
- else
31
- warn "vcdeps: unknown command #{command.inspect}\n\n"
32
- print_help
33
- 1
34
- end
35
- rescue Vcdeps::Error, Vcvars::Error, ArgumentError => e
36
- # Library messages already carry the "vcdeps: " prefix (§2.1); don't double
37
- # it when we re-emit them through the CLI.
38
- msg = e.message.to_s.sub(/\Avcdeps:\s*/, "")
39
- warn "vcdeps: #{msg}"
40
- 1
41
- rescue Interrupt
42
- 130
43
- end
44
-
45
- # --- commands ------------------------------------------------------------
46
-
47
- def cmd_version
48
- puts "vcdeps #{Vcdeps::VERSION}"
49
- 0
50
- end
51
-
52
- def cmd_doctor(argv)
53
- require "vcdeps/doctor"
54
- manifest = extract_opt(argv, "--manifest")
55
- deep = !argv.include?("--quick")
56
- checks = Doctor.run(manifest: manifest, deep: deep)
57
-
58
- checks.each do |c|
59
- puts "#{c.icon} #{c.label}"
60
- next unless c.detail && !c.detail.to_s.empty?
61
-
62
- c.detail.to_s.each_line { |line| puts " #{line.chomp}" }
63
- end
64
-
65
- fails = checks.count { |c| c.status == :fail }
66
- warns = checks.count { |c| c.status == :warn }
67
- puts
68
- if fails.zero?
69
- extra = warns.positive? ? " (#{warns} warning#{'s' if warns > 1})" : ""
70
- puts "Summary: no blocking problems#{extra}."
71
- 0
72
- else
73
- puts "Summary: #{fails} problem#{'s' if fails > 1} found — see remedies above."
74
- 1
75
- end
76
- end
77
-
78
- def cmd_where(_argv)
79
- tool = Vcdeps.tool
80
- if tool
81
- puts tool.to_s
82
- puts "exe: #{tool.exe}"
83
- puts "version: #{tool.version}"
84
- puts "source: #{tool.source}"
85
- 0
86
- else
87
- Vcdeps.tool! # raises ToolNotFound with the three remedies -> rescued as a one-liner
88
- 0
89
- end
90
- end
91
-
92
- def cmd_install(argv)
93
- manifest = extract_opt(argv, "--manifest")
94
- triplet = extract_opt(argv, "--triplet")
95
- force = argv.delete("--force") ? true : false
96
- manifest_dir = resolve_manifest(manifest)
97
-
98
- inst = Vcdeps.install!(manifest: manifest_dir, triplet: triplet, force: force,
99
- out: $stderr)
100
- puts inst.prefix
101
- 0
102
- end
103
-
104
- def cmd_vendor(argv)
105
- into = extract_opt(argv, "--into")
106
- manifest = extract_opt(argv, "--manifest")
107
- triplet = extract_opt(argv, "--triplet")
108
- licenses = !argv.delete("--no-licenses")
109
- shim = !argv.delete("--no-shim")
110
-
111
- if into.nil? || into.empty?
112
- warn "vcdeps vendor: --into DIR is required."
113
- warn "Example: vcdeps vendor --into lib/foo/vendor"
114
- return 1
115
- end
116
-
117
- inst = Vcdeps.install!(manifest: resolve_manifest(manifest), triplet: triplet,
118
- out: $stderr)
119
- written = Vcdeps.vendor!(inst, into: into, licenses: licenses, shim: shim,
120
- out: $stderr)
121
- if written.empty?
122
- puts "(nothing to vendor — static-md or header-only build)"
123
- else
124
- written.each { |f| puts f }
125
- end
126
- 0
127
- end
128
-
129
- def cmd_baseline(argv)
130
- manifest = extract_opt(argv, "--manifest")
131
- sha = Vcdeps.baseline!(manifest: resolve_manifest(manifest), out: $stderr)
132
- puts sha
133
- 0
134
- end
135
-
136
- def cmd_bootstrap(_argv)
137
- tool = Vcdeps.bootstrap!(out: $stderr)
138
- puts "Bootstrapped: #{tool}"
139
- 0
140
- end
141
-
142
- # --- helpers -------------------------------------------------------------
143
-
144
- # Manifest default: cwd; if cwd has no vcpkg.json and exactly one
145
- # ext/*/vcpkg.json exists, use that one (gem-layout DX).
146
- def resolve_manifest(explicit)
147
- return explicit if explicit && !explicit.empty?
148
- return Dir.pwd if File.exist?(File.join(Dir.pwd, "vcpkg.json"))
149
-
150
- ext_manifests = Dir[File.join(Dir.pwd, "ext", "*", "vcpkg.json")]
151
- return File.dirname(ext_manifests.first) if ext_manifests.size == 1
152
-
153
- Dir.pwd
154
- end
155
-
156
- # Pulls "--opt value" or "--opt=value" out of argv (mutating it). Returns the
157
- # value or nil; raises on a dangling flag.
158
- def extract_opt(argv, name)
159
- if (i = argv.index(name))
160
- value = argv[i + 1]
161
- if value.nil?
162
- argv.slice!(i, 1)
163
- raise Error, "#{name} requires a value."
164
- end
165
- argv.slice!(i, 2)
166
- return value
167
- end
168
- prefix = "#{name}="
169
- if (i = argv.index { |a| a.to_s.start_with?(prefix) })
170
- value = argv[i][prefix.length..]
171
- argv.slice!(i, 1)
172
- return value
173
- end
174
- nil
175
- end
176
-
177
- def print_help
178
- puts <<~HELP
179
- vcdeps #{Vcdeps::VERSION} — vcpkg-powered native dependencies for Ruby C extensions on Windows (MSVC).
180
-
181
- Usage:
182
- vcdeps doctor [--manifest DIR] [--quick]
183
- Diagnose vcpkg acquisition + mkmf wiring
184
- vcdeps where Show the resolved vcpkg (path, version, source)
185
- vcdeps install [--manifest DIR] [--triplet T] [--force]
186
- Pre-build the manifest's ports (CI / warm cache)
187
- vcdeps vendor --into DIR [--manifest DIR] [--triplet T] [--no-licenses] [--no-shim]
188
- install! then vendor DLLs + licenses + preload shim
189
- vcdeps baseline [--manifest DIR]
190
- Add/refresh "builtin-baseline" in vcpkg.json
191
- vcdeps bootstrap Create a private vcpkg under %LOCALAPPDATA%\\vcdeps
192
- vcdeps version
193
- vcdeps help
194
-
195
- Examples:
196
- vcdeps doctor --manifest ext/foo
197
- vcdeps install --manifest ext/foo --triplet x64-windows-static-md
198
- vcdeps vendor --into lib/foo/vendor --manifest ext/foo
199
- vcdeps baseline --manifest ext/foo
200
-
201
- In extconf.rb, `require "vcdeps/mkmf"; Vcdeps.mkmf!(vendor: ...)` does the
202
- install + wiring + vendoring automatically — no CLI step is needed to build.
203
- HELP
204
- end
205
- end
206
- end
data/lib/vcdeps/doctor.rb DELETED
@@ -1,263 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rbconfig"
4
- require "vcdeps"
5
- require "vcvars"
6
- require "vcvars/doctor"
7
-
8
- module Vcdeps
9
- # Diagnoses vcpkg acquisition + mkmf wiring, mirroring Vcvars::Doctor's
10
- # Check/icon/Summary pattern. Delegates the toolchain checks to vcvars (a hard
11
- # dependency whose Doctor is public API) and adds the vcdeps-specific checks
12
- # (§2.9): resolution, manifest baseline, opt-dir shadowing, Ruby-bin DLL
13
- # collisions, triplet sanity, binary cache, offline readiness, home hygiene.
14
- module Doctor
15
- module_function
16
-
17
- Check = Struct.new(:status, :label, :detail, keyword_init: true) do
18
- def icon
19
- { ok: "[ OK ]", warn: "[WARN]", fail: "[FAIL]", info: "[INFO]" }[status]
20
- end
21
- end
22
-
23
- # Returns an Array<Check>. `manifest` (a dir) enables the manifest/triplet
24
- # checks; `deep: true` adds the home-size INFO.
25
- def run(manifest: nil, deep: true)
26
- checks = []
27
- checks.concat(toolchain_checks)
28
- checks.concat(tool_checks)
29
- checks.concat(actions_root_checks)
30
- checks.concat(manifest_checks(manifest))
31
- checks.concat(optdir_checks)
32
- checks.concat(collision_checks(manifest))
33
- checks.concat(triplet_checks)
34
- checks.concat(cache_checks)
35
- checks.concat(offline_checks)
36
- checks.concat(home_checks(deep: deep))
37
- checks
38
- end
39
-
40
- def healthy?(checks)
41
- checks.none? { |c| c.status == :fail }
42
- end
43
-
44
- # 1. mswin Ruby + CRT linkage (delegates to vcvars). 2. vcvars viability.
45
- def toolchain_checks
46
- out = []
47
- out << if Vcvars::Doctor.mswin?
48
- Check.new(status: :ok, label: "Ruby is a native MSVC (mswin) build",
49
- detail: "#{RUBY_VERSION} #{RbConfig::CONFIG['arch']}, CRT " \
50
- "#{Vcvars::Doctor.crt_flag}")
51
- else
52
- Check.new(status: :fail, label: "Ruby is NOT an mswin build",
53
- detail: "vcdeps targets MSVC (mswin) Ruby only. Your Ruby is " \
54
- "#{RbConfig::CONFIG['arch']}. Not supported on MinGW/UCRT.")
55
- end
56
-
57
- inst = safe_locate
58
- out << if inst
59
- Check.new(status: :ok, label: "Visual Studio located", detail: inst.to_s)
60
- else
61
- Check.new(status: :warn, label: "No Visual Studio located via vcvars",
62
- detail: "vcdeps needs cl.exe to build extensions and to find the " \
63
- "VS-bundled vcpkg. Install \"Desktop development with " \
64
- "C++\"; run `vcvars doctor`.")
65
- end
66
- out << Check.new(status: (Vcvars.active? ? :ok : :info),
67
- label: "Developer environment #{Vcvars.active? ? 'ACTIVE' : 'not yet active'}",
68
- detail: Vcvars.active? ? nil : "vcdeps activates it on demand " \
69
- "(Vcvars.activate!); harmless if inactive now.")
70
- out
71
- end
72
-
73
- # 3. vcpkg resolution: source/path/version, or FAIL + the three remedies.
74
- def tool_checks
75
- tool = safe_tool
76
- if tool
77
- [Check.new(status: :ok, label: "vcpkg resolved (#{tool.source})",
78
- detail: "#{tool.exe}\n version #{tool.version}")]
79
- else
80
- [Check.new(status: :fail, label: "No usable vcpkg found",
81
- detail: "Remedies: (a) install the VS component " \
82
- "Microsoft.VisualStudio.Component.Vcpkg; (b) set " \
83
- "VCPKG_ROOT to an existing instance; (c) run " \
84
- "`vcdeps bootstrap` (or VCDEPS_BOOTSTRAP=1).")]
85
- end
86
- end
87
-
88
- # 4. VCPKG_INSTALLATION_ROOT set but invalid (issue #9269).
89
- def actions_root_checks
90
- root = ENV["VCPKG_INSTALLATION_ROOT"]
91
- return [] if root.nil? || root.empty?
92
- return [] if ToolFinder.valid_root(root)
93
-
94
- [Check.new(status: :warn, label: "VCPKG_INSTALLATION_ROOT is set but invalid",
95
- detail: "#{root.tr('/', '\\')} lacks vcpkg.exe/.vcpkg-root (the " \
96
- "runner-images #9269 bug). vcdeps SKIPS it; nothing to fix " \
97
- "unless you rely on it.")]
98
- end
99
-
100
- # 5. Manifest (when given): parses; builtin-baseline present or
101
- # vcpkg-configuration.json beside it — else FAIL with the baseline remedy.
102
- def manifest_checks(manifest)
103
- return [] if manifest.nil?
104
-
105
- dir = File.expand_path(manifest)
106
- path = File.join(dir, Manifest::MANIFEST_NAME)
107
- return [Check.new(status: :fail, label: "No manifest at #{path.tr('/', '\\')}",
108
- detail: "Create a vcpkg.json there.")] unless File.exist?(path)
109
-
110
- begin
111
- Manifest.load!(dir)
112
- [Check.new(status: :ok, label: "Manifest is valid and baselined",
113
- detail: path.tr("/", "\\"))]
114
- rescue ManifestError => e
115
- [Check.new(status: :fail, label: "Manifest problem", detail: e.message)]
116
- end
117
- end
118
-
119
- # 6. opt-dir shadowing: parse configure_args for --with-opt-dir.
120
- def optdir_checks
121
- args = RbConfig::CONFIG["configure_args"].to_s
122
- m = args.match(/--with-opt-dir=(\S+)/)
123
- return [] unless m
124
-
125
- [Check.new(status: :warn, label: "Ruby was built with --with-opt-dir",
126
- detail: "#{m[1]} is folded into EVERY extension's paths AHEAD of " \
127
- "appended dirs (opt-dir shadowing, R§8.4). vcdeps PREPENDS " \
128
- "its -I / -libpath so its vcpkg tree wins; no action needed.")]
129
- end
130
-
131
- # 7. Ruby-bin DLL collisions: scan the install's/vendored DLL base names
132
- # against Ruby's bindir.
133
- def collision_checks(manifest)
134
- return [] if manifest.nil?
135
-
136
- bindir = RbConfig::CONFIG["bindir"].to_s
137
- return [] if bindir.empty?
138
-
139
- names = manifest_dll_basenames(manifest)
140
- collisions = names.select { |n| File.exist?(File.join(bindir, n)) }
141
- return [] if collisions.empty?
142
-
143
- collisions.map do |n|
144
- Check.new(status: :warn, label: "DLL name collides with Ruby's bin: #{n}",
145
- detail: "#{File.join(bindir, n).tr('/', '\\')} exists; first-loaded " \
146
- "wins process-wide (R§9.2). The preload shim runs first " \
147
- "under normal require order.")
148
- end
149
- end
150
-
151
- # 8. Triplet sanity: resolved triplet vs §2.6 rules.
152
- def triplet_checks
153
- triplet = ENV["VCDEPS_TRIPLET"]
154
- triplet = (Triplet.default rescue nil) if triplet.nil? || triplet.empty?
155
- return [Check.new(status: :warn, label: "Cannot derive a triplet for this arch",
156
- detail: "Set VCDEPS_TRIPLET.")] if triplet.nil?
157
-
158
- begin
159
- Triplet.validate!(triplet)
160
- [Check.new(status: :ok, label: "Triplet OK: #{triplet}", detail: nil)]
161
- rescue TripletError => e
162
- [Check.new(status: :fail, label: "Triplet rejected", detail: e.message)]
163
- end
164
- end
165
-
166
- # 9. Binary cache: %LOCALAPPDATA%\vcpkg\archives (or override) + entry count.
167
- def cache_checks
168
- cache = ENV["VCPKG_DEFAULT_BINARY_CACHE"]
169
- cache = File.join(ENV["LOCALAPPDATA"] || "", "vcpkg", "archives") if cache.nil? || cache.empty?
170
- if File.directory?(cache)
171
- count = Dir[File.join(cache.tr("\\", "/"), "**", "*.zip")].size
172
- [Check.new(status: :info, label: "Binary cache present (#{count} archive(s))",
173
- detail: cache.tr("/", "\\"))]
174
- else
175
- [Check.new(status: :info, label: "Binary cache not yet created",
176
- detail: "#{cache.tr('/', '\\')} will hold built ports; first cold " \
177
- "build populates it.")]
178
- end
179
- end
180
-
181
- # 10. Registry/offline readiness: baseline tree under registries.
182
- def offline_checks
183
- reg = File.join(ENV["LOCALAPPDATA"] || "", "vcpkg", "registries")
184
- if File.directory?(reg) && !Dir.empty?(reg.tr("\\", "/"))
185
- [Check.new(status: :info, label: "Registry cache present (offline-capable)",
186
- detail: reg.tr("/", "\\"))]
187
- else
188
- [Check.new(status: :info, label: "Registry cache empty",
189
- detail: "First install of a baseline needs network for the registry " \
190
- "git-tree fetch.")]
191
- end
192
- end
193
-
194
- # 11. Vcdeps.home hygiene: spaces / non-ASCII WARN; size INFO on deep.
195
- def home_checks(deep:)
196
- out = []
197
- h = Vcdeps.home
198
- if h =~ /\s/
199
- out << Check.new(status: :warn, label: "VCDEPS_HOME path contains spaces",
200
- detail: "#{h} — naked -I flags and some port builds mishandle " \
201
- "spaces. Set VCDEPS_HOME to a short ASCII path.")
202
- elsif h =~ /[^\x00-\x7F]/
203
- out << Check.new(status: :warn, label: "VCDEPS_HOME path has non-ASCII characters",
204
- detail: "#{h} — some port build scripts mishandle it. Set " \
205
- "VCDEPS_HOME to a short ASCII path.")
206
- else
207
- out << Check.new(status: :ok, label: "vcdeps home path is clean", detail: h)
208
- end
209
-
210
- if deep && File.directory?(h)
211
- out << Check.new(status: :info, label: "vcdeps home size: #{home_size_mb(h)} MB",
212
- detail: h)
213
- end
214
- out
215
- end
216
-
217
- # --- internals -----------------------------------------------------------
218
-
219
- def safe_locate
220
- Vcvars.locate
221
- rescue StandardError
222
- nil
223
- end
224
-
225
- def safe_tool
226
- Vcdeps.tool
227
- rescue StandardError
228
- nil
229
- end
230
-
231
- # Best-effort: the DLL base names a manifest would vendor, read from an
232
- # already-installed tree if present (so doctor stays offline and cheap).
233
- def manifest_dll_basenames(manifest)
234
- dir = File.expand_path(manifest)
235
- tool = safe_tool
236
- return [] unless tool
237
-
238
- triplet = ENV["VCDEPS_TRIPLET"]
239
- triplet = (Triplet.default rescue nil) if triplet.nil? || triplet.empty?
240
- return [] unless triplet
241
-
242
- key = Manifest.key(dir, triplet, tool.version)
243
- root = File.join(Vcdeps.home, "installed", key)
244
- return [] unless File.directory?(root)
245
-
246
- inst = Installed.new(root: root, triplet: triplet, manifest_dir: dir,
247
- key: key, tool: tool)
248
- inst.dlls.map { |d| File.basename(d) }
249
- rescue StandardError
250
- []
251
- end
252
-
253
- def home_size_mb(dir)
254
- total = 0
255
- Dir.glob(File.join(dir.tr("\\", "/"), "**", "*"), File::FNM_DOTMATCH) do |f|
256
- total += File.size(f) if File.file?(f)
257
- end
258
- (total / (1024.0 * 1024.0)).round(1)
259
- rescue StandardError
260
- 0
261
- end
262
- end
263
- end