vcdeps 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 +43 -0
- data/LICENSE.txt +21 -0
- data/README.md +258 -0
- data/exe/vcdeps +6 -0
- data/lib/vcdeps/bootstrap.rb +163 -0
- data/lib/vcdeps/cli.rb +206 -0
- data/lib/vcdeps/doctor.rb +263 -0
- data/lib/vcdeps/installed.rb +139 -0
- data/lib/vcdeps/manifest.rb +100 -0
- data/lib/vcdeps/mkmf.rb +125 -0
- data/lib/vcdeps/runner.rb +145 -0
- data/lib/vcdeps/tool.rb +146 -0
- data/lib/vcdeps/triplet.rb +77 -0
- data/lib/vcdeps/vendor.rb +160 -0
- data/lib/vcdeps/version.rb +5 -0
- data/lib/vcdeps.rb +180 -0
- metadata +121 -0
data/lib/vcdeps/cli.rb
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
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
|
|
@@ -0,0 +1,263 @@
|
|
|
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
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vcdeps
|
|
4
|
+
# One installed port (name/version/triplet), parsed from an info-list filename.
|
|
5
|
+
Port = Struct.new(:name, :version, :triplet, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
# An immutable snapshot of one completed out-of-tree install. All paths are
|
|
8
|
+
# absolute Strings with backslash separators (display-normalized like
|
|
9
|
+
# Vcvars::Locator#win). Runtime DLLs are enumerated from the authoritative
|
|
10
|
+
# vcpkg\info\*.list files — NEVER guessed from port names (R§8.2) — and the
|
|
11
|
+
# debug/ tree is never exposed (R§8.4).
|
|
12
|
+
class Installed
|
|
13
|
+
attr_reader :root, :triplet, :manifest_dir, :key, :tool
|
|
14
|
+
|
|
15
|
+
# `tool` is nil for a --with-vcdeps-dir bypass install. `prefix:` overrides
|
|
16
|
+
# the computed <root>\<triplet> (the bypass passes the prefix dir directly).
|
|
17
|
+
def initialize(root:, triplet:, manifest_dir:, key:, tool:, prefix: nil)
|
|
18
|
+
@root = win(root)
|
|
19
|
+
@triplet = triplet
|
|
20
|
+
@manifest_dir = win(manifest_dir)
|
|
21
|
+
@key = key
|
|
22
|
+
@tool = tool
|
|
23
|
+
@prefix_override = prefix ? win(prefix) : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# <root>\<triplet> (or the bypass prefix override).
|
|
27
|
+
def prefix
|
|
28
|
+
return @prefix_override if @prefix_override
|
|
29
|
+
|
|
30
|
+
File.join(@root, @triplet).tr("/", "\\")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def include_dir
|
|
34
|
+
File.join(prefix, "include").tr("/", "\\")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The RELEASE lib dir — never debug\lib (R§8.4).
|
|
38
|
+
def lib_dir
|
|
39
|
+
File.join(prefix, "lib").tr("/", "\\")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def bin_dir
|
|
43
|
+
File.join(prefix, "bin").tr("/", "\\")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Accessor only; vcdeps never runs pkg-config (§5.14).
|
|
47
|
+
def pkgconfig_dir
|
|
48
|
+
File.join(prefix, "lib", "pkgconfig").tr("/", "\\")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The vcpkg metadata dir holding info\*.list (and status).
|
|
52
|
+
def info_dir
|
|
53
|
+
File.join(@root, "vcpkg", "info").tr("/", "\\")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Array<Port> parsed from <root>\vcpkg\info\<name>_<ver>_<triplet>.list
|
|
57
|
+
# filenames. In bypass mode (no tool) there is no info tree, so this is [].
|
|
58
|
+
def ports
|
|
59
|
+
list_files.filter_map { |path| parse_port(File.basename(path, ".list")) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Array<String> absolute paths of RELEASE runtime DLLs.
|
|
63
|
+
# Normal mode: read every info-list file, take entries ending in .dll that
|
|
64
|
+
# are NOT under a debug/ directory, resolve to absolute backslash paths.
|
|
65
|
+
# Bypass mode (no tool): glob bin\*.dll.
|
|
66
|
+
def dlls
|
|
67
|
+
if bypass?
|
|
68
|
+
Dir[File.join(bin_dir.tr("\\", "/"), "*.dll")].sort.map { |p| win(p) }
|
|
69
|
+
else
|
|
70
|
+
out = []
|
|
71
|
+
list_files.each do |list|
|
|
72
|
+
read_list_entries(list).each do |entry|
|
|
73
|
+
next unless entry =~ /\.dll\z/i
|
|
74
|
+
next if entry =~ %r{(?:\A|/)debug/}i
|
|
75
|
+
|
|
76
|
+
out << win(File.join(@root, entry))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
out.uniq.sort
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Hash{ "zlib" => "<prefix>\share\zlib\copyright", ... } for EXISTING files.
|
|
84
|
+
def copyright_files
|
|
85
|
+
result = {}
|
|
86
|
+
ports.each do |port|
|
|
87
|
+
cand = File.join(prefix, "share", port.name, "copyright")
|
|
88
|
+
result[port.name] = win(cand) if File.exist?(cand)
|
|
89
|
+
end
|
|
90
|
+
# Bypass mode has no ports; scan share\*\copyright directly.
|
|
91
|
+
if bypass?
|
|
92
|
+
share = File.join(prefix, "share").tr("\\", "/")
|
|
93
|
+
Dir[File.join(share, "*", "copyright")].each do |cp|
|
|
94
|
+
name = File.basename(File.dirname(cp))
|
|
95
|
+
result[name] ||= win(cp)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
result
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def bypass?
|
|
104
|
+
@tool.nil?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def win(path)
|
|
108
|
+
path.nil? ? path : path.tr("/", "\\")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The info-list files for THIS triplet (filenames end in _<triplet>.list).
|
|
112
|
+
def list_files
|
|
113
|
+
dir = info_dir.tr("\\", "/")
|
|
114
|
+
return [] unless File.directory?(dir)
|
|
115
|
+
|
|
116
|
+
Dir[File.join(dir, "*_#{@triplet}.list")].sort
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# info-list entries are "/"-separated relative paths under <root>.
|
|
120
|
+
def read_list_entries(path)
|
|
121
|
+
File.readlines(path, encoding: "UTF-8").map { |l| l.strip }.reject(&:empty?)
|
|
122
|
+
rescue StandardError
|
|
123
|
+
[]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# "<name>_<version>_<triplet>" -> Port. Split name on the FIRST "_" and
|
|
127
|
+
# triplet on the LAST "_" so versions containing "_" (rare) survive.
|
|
128
|
+
def parse_port(base)
|
|
129
|
+
first = base.index("_")
|
|
130
|
+
last = base.rindex("_")
|
|
131
|
+
return nil if first.nil? || last.nil? || first == last
|
|
132
|
+
|
|
133
|
+
name = base[0...first]
|
|
134
|
+
version = base[(first + 1)...last]
|
|
135
|
+
triplet = base[(last + 1)..]
|
|
136
|
+
Port.new(name: name, version: version, triplet: triplet)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|