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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cadbf9c576136cf47846e8c790890fd9225e01aea337edc0a83786b8aa4dfe11
4
+ data.tar.gz: 7e5a5401c339286ed4c03cf40536446c97ba02c1713525913bdee2e2107d29ab
5
+ SHA512:
6
+ metadata.gz: a774c5f893e1b28d4e9db594b45d45fc199f8e91543fd18b05c8b23295a7a89b458347d0173adf6fd54efe8e8566a002f065fb3409433f228f044b2ebfe0c70c
7
+ data.tar.gz: 0ba6b42201244938ac887d4e9755f1c25d398c6d62a50fd6d28f09033565d33f581a6b6b977aa257450d17d83f4328a433c094b37e34e53c7bfc2df34521797f
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-06-27
4
+
5
+ Initial release.
6
+
7
+ - `Vcdeps.tool` / `Vcdeps.tool!` — locate a usable vcpkg, validated
8
+ (vcpkg.exe + `.vcpkg-root`), in resolution order: explicit `VCPKG_ROOT`, the
9
+ VS developer-env `VCPKG_ROOT`, the VS-bundled `VC\vcpkg`, a validated
10
+ `VCPKG_INSTALLATION_ROOT` (GitHub Actions), then a private bootstrap. `tool!`
11
+ raises `Vcdeps::ToolNotFound` with three actionable remedies.
12
+ - `Vcdeps.bootstrap!` — create a private, registration-free vcpkg under
13
+ `%LOCALAPPDATA%\vcdeps\vcpkg` (download `vcpkg.exe` over TLS, then
14
+ `bootstrap-standalone`). Idempotent; never elevates or touches the registry.
15
+ - `Vcdeps.install!` — run `vcpkg install` for `<manifest>\vcpkg.json` into an
16
+ out-of-tree root keyed on (manifest, triplet, tool version), streaming child
17
+ output live. A `.vcdeps-complete` marker makes a current install a no-op;
18
+ `force:` bypasses it. Builds/caches live under `%LOCALAPPDATA%\vcdeps`, never
19
+ inside the gem (the deep-path port-build trap).
20
+ - `Vcdeps.vendor!` — sync an install's runtime DLLs, per-port `copyright` files,
21
+ and a generated standalone `Fiddle`-preload shim into a vendor dir. SYNC, not
22
+ copy: it owns `*.dll`, `licenses\*`, and `preload.rb`, deleting stale owned
23
+ files so a removed port leaves no orphan DLL.
24
+ - `Vcdeps.baseline!` — add/refresh `builtin-baseline` via
25
+ `vcpkg x-update-baseline --add-initial-baseline`.
26
+ - `Vcdeps.mkmf!` — the extconf entry point: activate vcvars, install the ports
27
+ (or bypass via `--with-vcdeps-dir`), PREPEND the include/lib paths so the
28
+ vcpkg tree wins over Ruby's build-time opt-dir, and vendor the DLLs.
29
+ - `Vcdeps.default_triplet` and `Vcdeps::Triplet.validate!` — derive
30
+ `<arch>-windows` from `RbConfig`; reject static-CRT triplets and arch
31
+ mismatches; `VCPKG_DEFAULT_TRIPLET` is deliberately ignored.
32
+ - `vcdeps` CLI — `doctor`, `where`, `install`, `vendor`, `baseline`,
33
+ `bootstrap`, `version`, `help`.
34
+ - `vcdeps doctor` — diagnoses resolution, manifest baseline, opt-dir shadowing,
35
+ Ruby-bin DLL collisions, triplet sanity, binary-cache / offline readiness, and
36
+ `VCDEPS_HOME` hygiene.
37
+
38
+ Manifest mode only. Telemetry is disabled in every child unless
39
+ `VCDEPS_METRICS=1`. Supported platform: native MSVC (mswin) Ruby, x64.
40
+ arm64-mswin is expected to work (arch-neutral) but is untested and unsupported
41
+ until an arm64-mswin Ruby distribution exists. Windows MSVC (mswin) Ruby only.
42
+
43
+ [0.1.0]: https://github.com/main-path/vcdeps/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,258 @@
1
+ # vcdeps
2
+
3
+ **vcpkg-powered native dependencies for Ruby C extensions on Windows — declare ports in a manifest, get correct mkmf flags and loadable runtime DLLs, no global state.**
4
+
5
+ Building a Ruby C extension against zlib, bzip2, or libxml2 on a native MSVC
6
+ (mswin) Ruby has two classic walls. First the compiler can't find the headers:
7
+
8
+ ```
9
+ fatal error C1083: Cannot open include file: 'zlib.h': No such file or directory
10
+ ```
11
+
12
+ You fix that, link, build a `.so` — and then it still won't load:
13
+
14
+ ```
15
+ LoadError: 126: The specified module could not be found - .../foo.so
16
+ ```
17
+
18
+ because Windows resolves a `.so`'s dependent DLLs through the standard search
19
+ order, which **never** looks in the `.so`'s own directory. vcdeps closes both
20
+ gaps: you declare your native dependencies in a `vcpkg.json` under `ext/`, call
21
+ `Vcdeps.mkmf!` from `extconf.rb`, and vcdeps locates (or bootstraps) vcpkg,
22
+ installs the ports **out of tree** with the dynamic-CRT triplet that matches an
23
+ `-MD` Ruby, prepends the include/lib paths so they win over Ruby's own opt-dir,
24
+ and vendors the runtime DLLs with a generated `Fiddle`-preload shim so the built
25
+ extension actually loads.
26
+
27
+ It is first of its kind. rake-compiler-dock is author-side MinGW-only
28
+ cross-compilation; rb_sys and ffi-compiler are GCC-oriented; nothing wired vcpkg
29
+ into mkmf before. vcdeps is the companion to [vcvars](https://github.com/main-path/vcvars)
30
+ (which loads the toolchain) the way vcpkg is the companion to MSVC.
31
+
32
+ ## API summary
33
+
34
+ | What | API |
35
+ |---|---|
36
+ | extconf entry point (install + wire + vendor) | `Vcdeps.mkmf!(vendor:)` |
37
+ | install a manifest's ports out of tree | `Vcdeps.install!(manifest:)` |
38
+ | sync DLLs + licenses + preload shim into a gem | `Vcdeps.vendor!(installed, into:)` |
39
+ | add/refresh `builtin-baseline` | `Vcdeps.baseline!(manifest:)` |
40
+ | locate vcpkg (or raise with remedies) | `Vcdeps.tool` / `Vcdeps.tool!` |
41
+ | bootstrap a private vcpkg | `Vcdeps.bootstrap!` |
42
+ | CLI | `vcdeps doctor / where / install / vendor / baseline / bootstrap` |
43
+
44
+ ## Requirements
45
+
46
+ - Windows with a native **MSVC (mswin)** Ruby (`RbConfig::CONFIG["target_os"]`
47
+ matches `/mswin/`). **Not supported on MinGW/UCRT.**
48
+ - Visual Studio 2017+ or Build Tools with the **Desktop development with C++**
49
+ workload (for `cl.exe` / `nmake`).
50
+ - A vcpkg instance. vcdeps finds one in this order: an explicit `VCPKG_ROOT`,
51
+ the VS-bundled vcpkg (the **vcpkg package manager** component —
52
+ `Microsoft.VisualStudio.Component.Vcpkg`, *Recommended* in that workload), a
53
+ validated `VCPKG_INSTALLATION_ROOT` (GitHub Actions), or a private instance
54
+ created by `vcdeps bootstrap`.
55
+ - [vcvars](https://github.com/main-path/vcvars) — installed automatically as a
56
+ runtime dependency.
57
+
58
+ ## Install
59
+
60
+ ```sh
61
+ gem install vcdeps
62
+ ```
63
+
64
+ ## Quick start
65
+
66
+ Four files turn a C extension into a vcpkg-backed gem.
67
+
68
+ ```json
69
+ // ext/foo/vcpkg.json — builtin-baseline is MANDATORY (the VS-bundled and
70
+ // standalone vcpkg are git-registry instances; vcdeps hard-fails without it).
71
+ // Get a SHA with: vcdeps baseline --manifest ext/foo
72
+ {
73
+ "name": "foo",
74
+ "version": "0.1.0",
75
+ "dependencies": ["zlib", "bzip2"],
76
+ "builtin-baseline": "e5a1490e1409d175932ef6014519e9ae149ddb7c"
77
+ }
78
+ ```
79
+
80
+ ```ruby
81
+ # ext/foo/extconf.rb
82
+ # frozen_string_literal: true
83
+ require "mkmf"
84
+
85
+ unless RbConfig::CONFIG["target_os"] =~ /mswin/
86
+ abort <<~MSG
87
+ foo requires a native Windows MSVC (mswin) Ruby — it links vcpkg-built
88
+ native libraries and is built with cl.exe. Your Ruby is "#{RbConfig::CONFIG['arch']}".
89
+ MSG
90
+ end
91
+
92
+ require "vcdeps/mkmf"
93
+ Vcdeps.mkmf!(vendor: File.expand_path("../../lib/foo/vendor", __dir__))
94
+
95
+ have_header("zlib.h") or abort "zlib.h not found — run `vcdeps doctor`"
96
+ have_library("zlib") or abort "zlib.lib not found" # bare name; LIBARG adds .lib
97
+ have_header("bzlib.h") or abort "bzlib.h not found"
98
+ have_library("bz2") or abort "bz2.lib not found" # import-lib name != port name
99
+ create_makefile("foo/foo")
100
+ ```
101
+
102
+ ```ruby
103
+ # lib/foo.rb
104
+ # frozen_string_literal: true
105
+ require "foo/version"
106
+ preload = File.expand_path("foo/vendor/preload.rb", __dir__)
107
+ require preload if File.exist?(preload) # shim absent for static-md / no-DLL builds
108
+ require "foo/foo" # the compiled extension
109
+ ```
110
+
111
+ ```ruby
112
+ # Rakefile — unchanged suite shape; vcdeps needs no Rake hook (extconf does it all)
113
+ require "vcvars/rake" # load the MSVC build env so `rake compile` just works
114
+ require "rake/extensiontask"
115
+ require "rake/testtask"
116
+ spec = Gem::Specification.load("foo.gemspec")
117
+ Rake::ExtensionTask.new("foo", spec) { |ext| ext.lib_dir = "lib/foo" }
118
+ Rake::TestTask.new(test: :compile) do |t|
119
+ t.libs << "test" << "lib"
120
+ t.test_files = FileList["test/**/test_*.rb"]
121
+ t.warning = false
122
+ end
123
+ task default: :test
124
+ ```
125
+
126
+ Then `rake compile && rake test`. End users override at install time with
127
+ standard mkmf semantics:
128
+
129
+ ```sh
130
+ gem install foo -- --with-vcdeps-triplet=x64-windows-static-md # link static, ship no DLLs
131
+ gem install foo -- --with-vcdeps-dir=C:\prebuilt\foo-deps # bypass vcpkg entirely
132
+ set VCDEPS_BOOTSTRAP=1 && gem install foo # consent to private bootstrap
133
+ ```
134
+
135
+ ## How loading works
136
+
137
+ Beside-the-`.so` **does not work**: Windows resolves an extension's dependent
138
+ DLLs as if loaded by module name only, and the search order never checks the
139
+ `.so`'s own directory. "It worked when I ran it from that folder" is the current
140
+ directory (search step 11) fooling you — move the cwd and it breaks. vcdeps
141
+ generates a `preload.rb` shim that `Fiddle.dlopen`s each vendored DLL by
142
+ **absolute path** before the extension loads, so the loaded-module list (search
143
+ step 4, consulted before PATH) satisfies the imports. The
144
+ `x64-windows-static-md` triplet sidesteps the whole problem by linking the
145
+ libraries statically into the `.so` (no DLLs to vendor). One caveat: Ruby's own
146
+ `bin` ships `zlib1.dll` / `ffi-8.dll` / `yaml.dll`; if one is already loaded
147
+ under that name, it wins process-wide — `vcdeps doctor` and `vendor!` warn on
148
+ base-name collisions.
149
+
150
+ ## Precompiled platform gems
151
+
152
+ The primary audience is **gem authors and their Windows CI** shipping
153
+ precompiled `x64-mswin64-140` gems so end users never compile. Build on Windows
154
+ CI, run `vcdeps vendor --into lib/<gem>/vendor`, then flip
155
+ `spec.platform = Gem::Platform.local`, drop `spec.extensions`, and include
156
+ `lib/<gem>/vendor/**/*` (the DLLs + `preload.rb` shim) in `spec.files`. The
157
+ source path (`gem install` → extconf → vcdeps → vcpkg) still works — it is the
158
+ author's dev loop and the fallback for exotic setups — but a cold libxml2-class
159
+ build at end-user install time is an accepted-but-discouraged slow path: vcdeps
160
+ streams vcpkg output live and leans on vcpkg's binary cache, so it is slow once,
161
+ never silent.
162
+
163
+ **License note:** vendored ports' `copyright` files ship in `vendor/licenses/`
164
+ for DLL builds **and** static-md builds (under static-md the port code is
165
+ statically linked into the shipped `.so`, so its copyright files are still
166
+ redistributed alongside it). Mind relink obligations for LGPL ports.
167
+
168
+ ## CI recipe
169
+
170
+ `windows-latest`, a self-provided mswin Ruby (ruby/setup-ruby's `mswin` build is
171
+ x64 ruby-master; there are no RubyInstaller mswin builds). Invoke vcpkg by
172
+ explicit path, let `vcvars/rake` + vcdeps do the rest, and cache the binary
173
+ archives:
174
+
175
+ ```yaml
176
+ - uses: actions/cache@v4
177
+ with:
178
+ path: ~/AppData/Local/vcpkg/archives # or $VCPKG_DEFAULT_BINARY_CACHE
179
+ key: vcpkg-${{ hashFiles('ext/**/vcpkg.json') }}-x64-windows-${{ env.VCPKG_TOOL_VERSION }}
180
+ - run: bundle exec rake compile
181
+ ```
182
+
183
+ The default `files` binary-cache provider needs no secrets and is immune to
184
+ provider churn (the `x-gha` provider was removed upstream in June 2025 — vcdeps
185
+ never references it). The NuGet-on-GitHub-Packages alternative is also viable;
186
+ note that Microsoft's own docs currently contradict themselves on whether the
187
+ workflow `GITHUB_TOKEN` (with `packages: write`) suffices or a classic PAT is
188
+ required — try `GITHUB_TOKEN` first and fall back to a PAT if pushes return 401.
189
+
190
+ ## Library API
191
+
192
+ ```ruby
193
+ require "vcdeps"
194
+
195
+ Vcdeps.default_triplet # => "x64-windows" (arm64 -> "arm64-windows")
196
+ Vcdeps.home # => "C:\\Users\\me\\AppData\\Local\\vcdeps"
197
+
198
+ Vcdeps.tool # => #<struct Vcdeps::Tool ... source=:devenv> or nil
199
+ Vcdeps.tool! # raises Vcdeps::ToolNotFound (with remedies) on a miss
200
+ Vcdeps.bootstrap! # private vcpkg under <home>\vcpkg (consent-gated)
201
+
202
+ inst = Vcdeps.install!(manifest: "ext/foo")
203
+ inst.prefix # => "...\\installed\\<key>\\x64-windows"
204
+ inst.ports # => [#<struct Vcdeps::Port name="zlib" version="1.3.1" ...>]
205
+ inst.dlls # => ["...\\bin\\zlib1.dll", ...] (release only, never debug)
206
+
207
+ Vcdeps.vendor!(inst, into: "lib/foo/vendor") # DLLs + licenses + preload.rb (a SYNC)
208
+ Vcdeps.install!(manifest: "ext/foo") # 2nd call: marker hit, returns in <100 ms
209
+ Vcdeps.baseline!(manifest: "ext/foo") # => "9f3dca…" (writes builtin-baseline)
210
+ ```
211
+
212
+ ## Configuration
213
+
214
+ | Env var | Effect |
215
+ |---|---|
216
+ | `VCDEPS_HOME` | State root (default `%LOCALAPPDATA%\vcdeps`); keep it short + ASCII. |
217
+ | `VCDEPS_TRIPLET` | Triplet override (between `--with-vcdeps-triplet` and the derived default). |
218
+ | `VCDEPS_BOOTSTRAP` | `=1` consents to a private bootstrap from a non-interactive `gem install`. |
219
+ | `VCDEPS_METRICS` | `=1` re-enables vcpkg telemetry (disabled by default — a `gem install` must not phone home). |
220
+
221
+ Honored vcpkg vars pass through untouched: `VCPKG_ROOT`,
222
+ `VCPKG_DEFAULT_BINARY_CACHE`, `VCPKG_BINARY_SOURCES`, `VCPKG_DOWNLOADS`.
223
+ **Ignored on purpose:** `VCPKG_DEFAULT_TRIPLET` — a machine-global default for
224
+ unrelated C++ projects must never silently change a Ruby extension's ABI.
225
+
226
+ ## Errors
227
+
228
+ ```
229
+ Vcdeps::Error (root; everything vcdeps raises)
230
+ ├── Vcdeps::ToolNotFound (no usable vcpkg anywhere — message lists remedies)
231
+ ├── Vcdeps::BootstrapError (private bootstrap failed: download / bootstrap-standalone)
232
+ ├── Vcdeps::ManifestError (vcpkg.json missing/unparsable/missing builtin-baseline)
233
+ ├── Vcdeps::TripletError (unknown arch, static-CRT triplet, arch mismatch)
234
+ └── Vcdeps::InstallError (`vcpkg install` exited nonzero; carries #command/#status/#log_tail)
235
+ ```
236
+
237
+ API-misuse uses Ruby's own `ArgumentError`/`TypeError`. No error ever prints a
238
+ multi-screen dump; `InstallError#log_tail` is capped at 4000 chars.
239
+
240
+ ## Limitations
241
+
242
+ - **Manifest mode only** — the VS-bundled and standalone vcpkg instances have no
243
+ classic mode; vcdeps cannot offer one.
244
+ - **x64 support.** arm64-mswin is expected to work (the code is arch-neutral and
245
+ triplets derive from `RbConfig`) but is untested and unsupported until an
246
+ arm64-mswin Ruby distribution exists.
247
+ - **Cold builds are slow** — libxml2-class ports are tens of minutes the first
248
+ time; the binary cache amortizes it. The first use of a baseline needs network
249
+ for the registry git-tree fetch.
250
+ - **DLL base-name collisions** across gems, and against Ruby's own `bin\zlib1.dll`
251
+ et al.: first-loaded wins. vcdeps warns; it cannot arbitrate.
252
+ - **Ctrl-C during an install** kills `vcpkg.exe` (so its install-root lock is
253
+ released) but not the helper build processes it spawned (`git`/`cmake`/`ninja`/
254
+ `cl`); they exit on their own. Wait a moment, then rerun.
255
+
256
+ ## License
257
+
258
+ [MIT](LICENSE.txt).
data/exe/vcdeps ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "vcdeps/cli"
5
+
6
+ exit Vcdeps::CLI.start(ARGV)
@@ -0,0 +1,163 @@
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