Class: Vcvars::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/vcvars/cli.rb

Overview

Command-line interface for the ‘vcvars` executable.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.start(argv) ⇒ Object



8
9
10
# File 'lib/vcvars/cli.rb', line 8

def self.start(argv)
  new.run(argv)
end

Instance Method Details

#cmd_doctor(argv) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/vcvars/cli.rb', line 46

def cmd_doctor(argv)
  require "vcvars/doctor"
  deep = !argv.include?("--quick")
  checks = Doctor.run(deep: deep)

  checks.each do |c|
    puts "#{c.icon} #{c.label}"
    next unless c.detail && !c.detail.to_s.empty?

    c.detail.to_s.each_line { |line| puts "       #{line.chomp}" }
  end

  fails = checks.count { |c| c.status == :fail }
  warns = checks.count { |c| c.status == :warn }
  puts
  if fails.zero?
    extra = warns.positive? ? " (#{warns} warning#{'s' if warns > 1})" : ""
    puts "Summary: no blocking problems#{extra}."
    0
  else
    puts "Summary: #{fails} problem#{'s' if fails > 1} found — see remedies above."
    1
  end
end

#cmd_env(argv) ⇒ Object



86
87
88
89
90
91
92
# File 'lib/vcvars/cli.rb', line 86

def cmd_env(argv)
  format = (extract_opt(argv, "--format") || "bat").downcase
  arch   = extract_opt(argv, "--arch")
  delta  = arch ? Vcvars.env(arch: arch) : Vcvars.env
  print_env(delta, format)
  0
end

#cmd_exec(argv) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/vcvars/cli.rb', line 94

def cmd_exec(argv)
  arch, command = parse_exec_args(argv)
  if command.nil? || command.empty?
    warn "vcvars exec: no command given."
    warn "Example: vcvars exec -- rake compile"
    return 1
  end

  arch ? Vcvars.activate!(arch: arch) : Vcvars.activate!

  ok = system(*command)
  if ok.nil?
    warn "vcvars exec: failed to run #{command.first.inspect} (not found?)."
    return 127
  end
  $?.exitstatus || (ok ? 0 : 1)
end

#cmd_new(argv) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/vcvars/cli.rb', line 112

def cmd_new(argv)
  require "vcvars/scaffold"
  force = argv.delete("--force") ? true : false
  dir   = extract_opt(argv, "--dir")
  name  = argv.shift

  unless name
    warn "vcvars new: missing NAME."
    warn "Example: vcvars new my_ext"
    return 1
  end

  scaffold = Scaffold.new(name, dir: dir)
  created  = scaffold.generate(force: force)

  puts "Created gem '#{scaffold.name}' (module #{scaffold.module_name}) in #{scaffold.dest}:"
  prefix = scaffold.dest + File::SEPARATOR
  created.each { |f| puts "  #{f.start_with?(prefix) ? f[prefix.length..] : f}" }
  puts
  puts "Next steps:"
  puts "  cd #{scaffold.dest}"
  puts "  bundle install"
  puts "  rake compile && rake test"
  0
end

#cmd_versionObject

— commands ————————————————————



41
42
43
44
# File 'lib/vcvars/cli.rb', line 41

def cmd_version
  puts "vcvars #{Vcvars::VERSION}"
  0
end

#cmd_where(argv) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/vcvars/cli.rb', line 71

def cmd_where(argv)
  arch = extract_opt(argv, "--arch")
  inst = arch ? Vcvars.locate(arch: arch) : Vcvars.locate
  if inst
    puts inst.to_s
    puts "vcvars script: #{inst.vcvars}"
    puts "target arch:   #{inst.arch}"
    0
  else
    warn "vcvars: no Visual Studio with the C++ tools was found."
    warn "Install the \"Desktop development with C++\" workload, then retry."
    1
  end
end

#extract_opt(argv, name) ⇒ Object

Pulls “–opt value” or “–opt=value” out of argv (mutating it). Returns the value or nil.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/vcvars/cli.rb', line 163

def extract_opt(argv, name)
  if (i = argv.index(name))
    value = argv[i + 1]
    if value.nil?
      argv.slice!(i, 1)
      raise Error, "#{name} requires a value."
    end
    argv.slice!(i, 2)
    return value
  end
  prefix = "#{name}="
  if (i = argv.index { |a| a.to_s.start_with?(prefix) })
    value = argv[i][prefix.length..]
    argv.slice!(i, 1)
    return value
  end
  nil
end

#parse_exec_args(argv) ⇒ Object

Splits exec args into [arch, command]. Flags (a leading –arch, or anything before a “–” separator) are parsed; the rest is the command.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/vcvars/cli.rb', line 142

def parse_exec_args(argv)
  if (sep = argv.index("--"))
    head = argv[0...sep]
    command = argv[(sep + 1)..]
    [extract_opt(head, "--arch"), command]
  else
    rest = argv.dup
    arch = nil
    # Only honor flags that appear before the command.
    if rest.first == "--arch"
      rest.shift
      arch = rest.shift
    elsif rest.first.to_s.start_with?("--arch=")
      arch = rest.shift.split("=", 2)[1]
    end
    [arch, rest]
  end
end


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/vcvars/cli.rb', line 182

def print_env(delta, format)
  keys = delta.keys.sort_by(&:upcase)
  case format
  when "bat", "cmd"
    keys.each { |k| puts %{set "#{k}=#{delta[k]}"} }
  when "powershell", "ps", "ps1"
    keys.each { |k| puts %{$env:#{k} = '#{delta[k].to_s.gsub("'", "''")}'} }
  when "sh", "bash"
    keys.each { |k| puts %{export #{k}="#{sh_escape(delta[k])}"} }
  when "dotenv", "env"
    keys.each { |k| puts "#{k}=#{delta[k]}" }
  when "json"
    require "json"
    puts JSON.pretty_generate(delta)
  else
    raise Error, "unknown --format #{format.inspect} " \
      "(use bat, powershell, sh, dotenv, or json)."
  end
end


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/vcvars/cli.rb', line 206

def print_help
  puts <<~HELP
    vcvars #{Vcvars::VERSION} — load the MSVC build environment for native Ruby extensions.

    Usage:
      vcvars doctor [--quick]          Diagnose the MSVC extension toolchain
      vcvars where  [--arch ARCH]      Show the located Visual Studio + vcvars script
      vcvars env    [--arch ARCH] [--format bat|powershell|sh|dotenv|json]
                                       Print the MSVC env vars (the vcvars delta)
      vcvars exec   [--arch ARCH] -- CMD [ARGS...]
                                       Run CMD inside the MSVC environment
      vcvars new    NAME [--dir DIR] [--force]
                                       Scaffold a new MSVC-ready C-extension gem
      vcvars version
      vcvars help

    Examples:
      vcvars doctor
      vcvars exec -- rake compile
      vcvars exec -- nmake
      vcvars env --format powershell | Invoke-Expression
      vcvars new my_ext

    In a Rakefile, `require "vcvars/rake"` before Rake::ExtensionTask.new to
    auto-load the toolchain so `rake compile` works without a Developer Prompt.
  HELP
end

#run(argv) ⇒ Object

Returns a process exit status (Integer).



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/vcvars/cli.rb', line 13

def run(argv)
  argv = argv.dup
  command = argv.shift

  case command
  when "doctor"                      then cmd_doctor(argv)
  when "where"                       then cmd_where(argv)
  when "env"                         then cmd_env(argv)
  when "exec"                        then cmd_exec(argv)
  when "new"                         then cmd_new(argv)
  when "version", "-v", "--version"  then cmd_version
  when nil, "help", "-h", "--help"   then print_help; 0
  else
    warn "vcvars: unknown command #{command.inspect}\n\n"
    print_help
    1
  end
rescue Vcvars::Error, ArgumentError => e
  # ArgumentError covers user input like an unsupported `--arch` value, which
  # Locator.vcvars_name raises; report it the same clean way as other errors.
  warn "vcvars: #{e.message}"
  1
rescue Interrupt
  130
end

#sh_escape(value) ⇒ Object



202
203
204
# File 'lib/vcvars/cli.rb', line 202

def sh_escape(value)
  value.to_s.gsub(/[\\"$`]/) { |c| "\\#{c}" }
end