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,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rbconfig"
4
-
5
- module Vcdeps
6
- # Derives the vcpkg triplet for the running Ruby and validates user-supplied
7
- # triplets. The default is the dynamic-CRT, dynamic-library triplet that
8
- # matches an `-MD` mswin Ruby (x64 -> "x64-windows"); static-CRT triplets are
9
- # rejected (the LNK2005/dual-heap hazard, R§4).
10
- #
11
- # No "x64" literal lives in code: the architecture token is derived from
12
- # RbConfig::CONFIG["arch"], so arm64-mswin derives "arm64-windows" for free.
13
- module Triplet
14
- module_function
15
-
16
- # Maps a Ruby arch string to its vcpkg architecture token, or nil if
17
- # unrecognized. Mirrors Vcvars::Doctor.arch's regexes.
18
- # "x64-mswin64_140" -> "x64"
19
- # "arm64-mswin64_140" -> "arm64"
20
- # "i386-mswin32" -> "x86"
21
- def arch_token(arch)
22
- a = arch.to_s
23
- return "x64" if a =~ /\A(?:x64|x86_64|amd64)/i
24
- return "arm64" if a =~ /arm64|aarch64/i
25
- return "x86" if a =~ /\A(?:i[3-6]86|x86)/i
26
- nil
27
- end
28
-
29
- # Known architecture prefixes a triplet may start with — used to detect an
30
- # arch mismatch against the running Ruby.
31
- ARCH_TOKENS = %w[x64 x86 arm64 arm].freeze
32
-
33
- # The default triplet for the given Ruby arch (dynamic CRT, dynamic libs).
34
- # Raises Vcdeps::TripletError for an unrecognized arch.
35
- def default(arch = RbConfig::CONFIG["arch"])
36
- token = arch_token(arch)
37
- raise TripletError, "vcdeps: cannot derive a vcpkg triplet for arch " \
38
- "#{arch.inspect} (recognized: x64, x86, arm64). Set VCDEPS_TRIPLET or " \
39
- "pass --with-vcdeps-triplet=<triplet> explicitly." if token.nil?
40
-
41
- "#{token}-windows"
42
- end
43
-
44
- # Validate a triplet against §2.6 and return it unchanged, or raise
45
- # Vcdeps::TripletError. `arch` is the running Ruby's arch (for the mismatch
46
- # check); pass it explicitly in tests to exercise the forward-compat path.
47
- def validate!(triplet, arch: RbConfig::CONFIG["arch"])
48
- t = triplet.to_s
49
-
50
- unless t =~ /\A[a-z0-9][a-z0-9-]*\z/
51
- raise TripletError, "vcdeps: invalid triplet #{triplet.inspect} " \
52
- "(must match /\\A[a-z0-9][a-z0-9-]*\\z/). Run `vcdeps doctor`."
53
- end
54
-
55
- # Reject a static-CRT triplet (-static suffix). static-md (static libs +
56
- # dynamic CRT) is fine; plain -static sets VCPKG_CRT_LINKAGE static.
57
- if t =~ /-static\z/
58
- raise TripletError, "vcdeps: triplet #{triplet.inspect} uses a STATIC " \
59
- "CRT, which is ABI-incompatible with this -MD Ruby (LNK2005 / dual " \
60
- "heap). Use #{t.sub(/-static\z/, '-static-md')} (static libs, dynamic " \
61
- "CRT — no DLLs to vendor) or the default #{default(arch)}."
62
- end
63
-
64
- # If the triplet leads with a known arch token that differs from this
65
- # Ruby's arch, it would LNK1112 at link time.
66
- lead = t.split("-", 2).first
67
- ruby_token = arch_token(arch)
68
- if ARCH_TOKENS.include?(lead) && ruby_token && lead != ruby_token
69
- raise TripletError, "vcdeps: triplet #{triplet.inspect} targets #{lead} " \
70
- "but this Ruby is #{ruby_token} (#{arch}); linking would fail with " \
71
- "LNK1112. Use a #{ruby_token}-* triplet (default #{default(arch)})."
72
- end
73
-
74
- t
75
- end
76
- end
77
- end
data/lib/vcdeps/vendor.rb DELETED
@@ -1,160 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "rbconfig"
5
- require "vcdeps/version"
6
-
7
- module Vcdeps
8
- # Syncs an Installed's runtime DLLs, per-port copyright files, and a standalone
9
- # Fiddle-preload shim into a vendor directory (§2.2/§2.7). This is a SYNC, not
10
- # a copy: vendor! OWNS <into>\*.dll, <into>\licenses\* and <into>\preload.rb —
11
- # it overwrites the computed write list and DELETES any owned file no longer on
12
- # it (so a port removed from vcpkg.json leaves no stale DLL the shim would
13
- # preload and the precompiled-gem glob would ship). Nothing else under `into`
14
- # is touched, so authors may keep unrelated assets there.
15
- module Vendor
16
- module_function
17
-
18
- SHIM_NAME = "preload.rb"
19
- LICENSES_DIR = "licenses"
20
-
21
- # Returns the list of files written (absolute, backslashed). See §2.2 for the
22
- # four-case table; stale owned files are deleted whenever `into` exists.
23
- def sync!(installed, into:, licenses: true, shim: true, out: $stderr)
24
- dlls = installed.dlls
25
- copyrights = licenses ? installed.copyright_files : {}
26
-
27
- # The write plan: a DLL is vendored only when there is something to vendor;
28
- # the shim is written only when DLLs exist; licenses ship whenever copyright
29
- # files exist (including static-md, where there are no DLLs).
30
- write_dlls = dlls
31
- write_shim = shim && !dlls.empty?
32
- write_copies = copyrights
33
-
34
- # Nothing to write AND nothing to clean (into absent) -> [] without
35
- # creating `into` (§2.2 last case).
36
- if write_dlls.empty? && write_copies.empty? && !write_shim &&
37
- !File.directory?(into)
38
- return []
39
- end
40
-
41
- written = []
42
- FileUtils.mkdir_p(into)
43
-
44
- # Owned DLLs we intend to keep (base names), for the stale sweep.
45
- kept_dll_names = []
46
- write_dlls.each do |src|
47
- base = File.basename(src)
48
- dest = File.join(into, base)
49
- FileUtils.cp(src, dest)
50
- kept_dll_names << base.downcase
51
- written << win(dest)
52
- warn_on_ruby_bin_collision(base, out)
53
- end
54
-
55
- # Licenses.
56
- kept_license_names = []
57
- unless write_copies.empty?
58
- ldir = File.join(into, LICENSES_DIR)
59
- FileUtils.mkdir_p(ldir)
60
- write_copies.each do |port, src|
61
- dest = File.join(ldir, "#{port}-copyright.txt")
62
- FileUtils.cp(src, dest)
63
- kept_license_names << File.basename(dest).downcase
64
- written << win(dest)
65
- end
66
- end
67
-
68
- # Shim.
69
- shim_path = File.join(into, SHIM_NAME)
70
- if write_shim
71
- File.write(shim_path, shim_source)
72
- written << win(shim_path)
73
- end
74
-
75
- # Stale sweep of OWNED files only.
76
- sweep_stale!(into, kept_dll_names, kept_license_names, write_shim)
77
-
78
- written
79
- end
80
-
81
- # The generated preload shim (exact §2.7 template). Standalone: stdlib only,
82
- # no vcdeps require, safe to require twice, safe with zero DLLs present.
83
- def shim_source
84
- <<~SHIM
85
- # frozen_string_literal: true
86
- #
87
- # Generated by vcdeps #{Vcdeps::VERSION}. Do not edit; regenerate with `vcdeps vendor`.
88
- #
89
- # Preloads the vendored vcpkg runtime DLLs by ABSOLUTE path before the C
90
- # extension loads. Windows resolves an extension's dependent DLLs via the
91
- # standard search order, which checks the loaded-module list (step 4) long
92
- # before PATH (step 12) and NEVER checks the .so's own directory — so loading
93
- # each DLL here, by full path, is what makes `require "<gem>/<gem>"` work.
94
- #
95
- # The dlopen handles are intentionally leaked for process lifetime: the DLLs
96
- # must stay loaded as long as the extension is loaded.
97
- require "fiddle"
98
-
99
- pending = Dir[File.join(__dir__, "*.dll")].sort
100
- until pending.empty?
101
- progressed = false
102
- pending.delete_if do |dll|
103
- Fiddle.dlopen(dll)
104
- progressed = true
105
- true
106
- rescue Fiddle::DLError
107
- false # depends on a sibling not yet loaded — retry next pass
108
- end
109
- break unless progressed # truly unloadable; let the require raise its own LoadError
110
- end
111
- SHIM
112
- end
113
-
114
- # --- internals -----------------------------------------------------------
115
-
116
- # Delete any *.dll directly in `into` not on the kept list, any licenses\*
117
- # file not kept, and a stale shim when no shim is being written.
118
- def sweep_stale!(into, kept_dll_names, kept_license_names, write_shim)
119
- Dir[File.join(into.tr("\\", "/"), "*.dll")].each do |dll|
120
- File.delete(dll) unless kept_dll_names.include?(File.basename(dll).downcase)
121
- end
122
-
123
- ldir = File.join(into.tr("\\", "/"), LICENSES_DIR)
124
- if File.directory?(ldir)
125
- Dir[File.join(ldir, "*")].each do |f|
126
- next unless File.file?(f)
127
-
128
- File.delete(f) unless kept_license_names.include?(File.basename(f).downcase)
129
- end
130
- # Remove an empty licenses dir we no longer populate.
131
- begin
132
- Dir.rmdir(ldir) if Dir.empty?(ldir)
133
- rescue SystemCallError
134
- nil
135
- end
136
- end
137
-
138
- shim = File.join(into.tr("\\", "/"), SHIM_NAME)
139
- File.delete(shim) if !write_shim && File.exist?(shim)
140
- end
141
-
142
- # Warn when a vendored DLL base name also exists in Ruby's bindir
143
- # (zlib1.dll/ffi-8.dll/yaml.dll shadowing, R§9.2) — first-loaded wins.
144
- def warn_on_ruby_bin_collision(base, out)
145
- bindir = RbConfig::CONFIG["bindir"].to_s
146
- return if bindir.empty?
147
-
148
- ruby_copy = File.join(bindir, base)
149
- return unless File.exist?(ruby_copy)
150
-
151
- out&.puts("[vcdeps] WARNING: #{base} also ships in #{bindir.tr('/', '\\')} " \
152
- "— the copy loaded first wins process-wide; preload.rb runs " \
153
- "first under normal require order.")
154
- end
155
-
156
- def win(path)
157
- path.nil? ? path : path.tr("/", "\\")
158
- end
159
- end
160
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vcdeps
4
- VERSION = "0.1.0"
5
- end