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
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,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
|