Module: Vcdeps::Bootstrap

Defined in:
lib/vcdeps/bootstrap.rb

Overview

Creates a private, registration-free vcpkg instance under \vcpkg using the same mechanism as Microsoft's vcpkg-init (R§2.4): download vcpkg.exe from the vcpkg-tool GitHub release, then run vcpkg.exe bootstrap-standalone with VCPKG_ROOT=\vcpkg in the child env. Never elevates, never touches the registry, never writes outside home.

Constant Summary collapse

OPEN_TIMEOUT =
30
READ_TIMEOUT =
30
MAX_REDIRECTS =
5

Class Method Summary collapse

Class Method Details

.absolutize(location, base) ⇒ Object



152
153
154
155
# File 'lib/vcdeps/bootstrap.rb', line 152

def absolutize(location, base)
  u = URI.parse(location)
  u.absolute? ? location : (base + location).to_s
end

.bootstrap_standalone!(exe, root, out) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/vcdeps/bootstrap.rb', line 102

def bootstrap_standalone!(exe, root, out)
  env = { "VCPKG_ROOT" => root }
  env["VCPKG_DISABLE_METRICS"] = "1" unless ENV["VCDEPS_METRICS"] == "1"

  combined = +""
  Open3.popen2e(env, exe, "bootstrap-standalone") do |_stdin, oe, wt|
    pid = wt.pid
    oe.binmode
    begin
      oe.each_line do |raw|
        line = raw.dup.force_encoding("UTF-8").scrub("")
        combined << line
        out&.print(line)
      end
      status = wt.value
      unless status.success?
        raise BootstrapError, "vcdeps: `vcpkg bootstrap-standalone` exited " \
          "#{status.exitstatus}. Output tail: " \
          "#{combined[-500..] || combined}"
      end
    rescue BootstrapError
      raise
    rescue Exception # rubocop:disable Lint/RescueException
      # ANY abnormal unwind (Interrupt, Timeout, a raise from the out block):
      # kill THIS child before Open3's block teardown runs `wait_thr.join`,
      # which would otherwise BLOCK until the orphaned bootstrap child (busy
      # downloading/extracting/cloning) exits on its own. Same discipline as
      # Runner.run — vcpkg.exe never outlives the call (§3.2).
      kill_and_close(pid, oe)
      raise
    end
  end
end

.cleanup_tmp(tmp) ⇒ Object



157
158
159
160
161
# File 'lib/vcdeps/bootstrap.rb', line 157

def cleanup_tmp(tmp)
  File.unlink(tmp) if tmp && File.exist?(tmp)
rescue StandardError
  nil
end

.download!(url, dest, redirects_left = MAX_REDIRECTS) ⇒ Object

Net::HTTP GET to dest, following up to MAX_REDIRECTS redirects, with TLS and bounded timeouts. Writes atomically (temp then rename).



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/vcdeps/bootstrap.rb', line 53

def download!(url, dest, redirects_left = MAX_REDIRECTS)
  uri = URI.parse(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = (uri.scheme == "https")
  http.open_timeout = OPEN_TIMEOUT
  http.read_timeout = READ_TIMEOUT

  tmp = "#{dest}.tmp.#{Process.pid}"
  http.start do |conn|
    request = Net::HTTP::Get.new(uri)
    conn.request(request) do |response|
      case response
      when Net::HTTPSuccess
        File.open(tmp, "wb") do |io|
          response.read_body { |chunk| io.write(chunk) }
        end
      when Net::HTTPRedirection
        raise BootstrapError, "vcdeps: too many redirects fetching " \
          "#{url}" if redirects_left <= 0

        location = response["location"]
        next_url = absolutize(location, uri)
        # The bootstrap artifact is executed, so it MUST arrive over TLS
        # end-to-end. GitHub release downloads normally redirect (to
        # objects.githubusercontent.com); honoring a `Location: http://...`
        # would silently drop TLS and let an on-path attacker substitute the
        # executable. Refuse any non-HTTPS redirect target.
        unless URI.parse(next_url).scheme == "https"
          raise BootstrapError, "vcdeps: refusing non-HTTPS redirect to " \
            "#{next_url} while fetching vcpkg.exe (TLS downgrade)"
        end
        return download!(next_url, dest, redirects_left - 1)
      else
        raise BootstrapError, "vcdeps: download of #{url} failed: " \
          "#{response.code} #{response.message}"
      end
    end
  end

  File.rename(tmp, dest)
rescue BootstrapError
  cleanup_tmp(tmp)
  raise
rescue StandardError => e
  cleanup_tmp(tmp)
  raise BootstrapError, "vcdeps: download of #{url} failed: #{e.class}: " \
    "#{e.message}"
end

.kill_and_close(pid, io) ⇒ Object

Shared abnormal-unwind teardown for the two child-spawn sites (here and Runner.run share the same shape so neither can drift): TerminateProcess the child, then close the pipe so Open3's drain returns at once.



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/vcdeps/bootstrap.rb', line 139

def kill_and_close(pid, io)
  begin
    Process.kill(:KILL, pid)
  rescue Errno::ESRCH, RangeError
    nil
  end
  begin
    io.close
  rescue IOError
    nil
  end
end

.run!(out: $stderr) ⇒ Object

Idempotent: an already-valid private root returns immediately. Progress lines to out. Raises Vcdeps::BootstrapError on any failure.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/vcdeps/bootstrap.rb', line 24

def run!(out: $stderr)
  root = File.join(Vcdeps.home, "vcpkg").tr("/", "\\")

  if (exe = ToolFinder.valid_root(root))
    return ToolFinder.build_tool(exe, :private)
  end

  FileUtils.mkdir_p(root)
  exe_path = File.join(root, "vcpkg.exe")

  out&.puts("[vcdeps] downloading vcpkg.exe (~7 MB) from " \
            "#{ToolFinder::BOOTSTRAP_URL} ...")
  download!(ToolFinder::BOOTSTRAP_URL, exe_path)

  out&.puts("[vcdeps] running `vcpkg bootstrap-standalone` in #{root} ...")
  bootstrap_standalone!(exe_path, root, out)

  resolved = ToolFinder.valid_root(root)
  unless resolved
    raise BootstrapError, "vcdeps: bootstrap-standalone completed but " \
      "#{root} is not a valid vcpkg root (.vcpkg-root missing). Run " \
      "`vcdeps doctor`."
  end

  ToolFinder.build_tool(resolved, :private)
end