mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Add vintner doctor for diagnosing a broken wine/toolchain setup
A misconfigured environment (wine missing, msitools not installed, a partially-built toolchain) otherwise only surfaces as a wine-specific error buried deep inside a build. `vintner doctor` checks wine itself (found and actually runs), the optional extraction tools download needs, and every installed <dest>/bin/<arch> toolchain's on-disk layout, printing a pass/fail checklist and exiting non-zero if anything's broken.
This commit is contained in:
@@ -22,6 +22,7 @@ approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
||||
- [Quick start](#quick-start)
|
||||
- [Commands](#commands)
|
||||
- [Invoking tools without PATH](#invoking-tools-without-path)
|
||||
- [Diagnosing problems (vintner doctor)](#diagnosing-problems-vintner-doctor)
|
||||
- [Building drivers (WDK)](#building-drivers-wdk)
|
||||
- [Building against D3DX9 (DirectX SDK)](#building-against-d3dx9-directx-sdk)
|
||||
- [Automated/scripted builds](#automatedscripted-builds)
|
||||
@@ -46,9 +47,9 @@ depending on the name it's invoked as.
|
||||
then runs the real `.exe` under `wine`/`wine64`, and rewrites `z:\...`
|
||||
paths back to Unix paths in the output, so your build system's error
|
||||
parsing keeps working.
|
||||
- As `vintner`: it exposes the `download`, `install`, `env`, `version`
|
||||
and `completion` subcommands below (short aliases: `dl`, `i`, `e`, `v`;
|
||||
`help`/`h` prints usage).
|
||||
- As `vintner`: it exposes the `download`, `install`, `env`, `version`,
|
||||
`doctor` and `completion` subcommands below (short aliases: `dl`, `i`,
|
||||
`e`, `v`; `help`/`h` prints usage).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -113,6 +114,7 @@ vintner download (dl) --accept-license [--dest <dir>] [options] fetch and unpa
|
||||
vintner install (i) [dir] wire up wrappers for a downloaded MSVC
|
||||
vintner env (e) --bin <dir>/bin/<arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
vintner version (v) print the version
|
||||
vintner doctor check wine/toolchain setup
|
||||
vintner help (h) print usage
|
||||
vintner completion bash|zsh print a shell completion script
|
||||
```
|
||||
@@ -154,6 +156,20 @@ than one is installed), otherwise defaults to
|
||||
`~/.vintner/bin/<host-arch>`, the layout a plain `vintner download &&
|
||||
vintner install` with no `--dest` override produces.
|
||||
|
||||
## Diagnosing problems (vintner doctor)
|
||||
|
||||
```bash
|
||||
vintner doctor
|
||||
```
|
||||
|
||||
Checks the things vintner actually needs at runtime — that `wine`/`wine64`
|
||||
is on `PATH` and actually runs, that `msitools`/`cabextract` are present,
|
||||
and that every installed `<dest>/bin/<arch>` toolchain (or the one
|
||||
`VINTNER_BIN` points at) has its MSVC/SDK/MSBuild directories in place —
|
||||
and prints a pass/fail checklist. Exits non-zero if anything failed.
|
||||
Useful before filing a bug, or after a `download`/`install` that seemed to
|
||||
finish but left tools failing in confusing ways.
|
||||
|
||||
## Building drivers (WDK)
|
||||
|
||||
`--with-wdk` also fetches the Windows Driver Kit: headers, import libs,
|
||||
|
||||
@@ -49,7 +49,7 @@ const downloadFlags = "--dest --cache --major --preview --manifest --accept-lice
|
||||
// plus every short alias) - unlike the wrapped-tool names, these really are
|
||||
// fixed enough to hand-maintain: adding one is rare and always touches
|
||||
// main.go's dispatch switch right next to this file anyway.
|
||||
const subcommandNames = "download dl install i env e version v help h completion"
|
||||
const subcommandNames = "download dl install i env e version v doctor help h completion"
|
||||
|
||||
func bashCompletionScript() string {
|
||||
return `# vintner bash completion - eval "$(vintner completion bash)"
|
||||
@@ -104,6 +104,7 @@ _vintner() {
|
||||
'e:alias for env'
|
||||
'version:print the version'
|
||||
'v:alias for version'
|
||||
'doctor:check wine/toolchain setup'
|
||||
'help:print usage'
|
||||
'h:alias for help'
|
||||
'completion:print a shell completion script'
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/i18n"
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
// runDoctor checks the pieces vintner actually needs at runtime - wine
|
||||
// itself, the optional extraction tools download needs, and every
|
||||
// installed <dest>/bin/<arch> toolchain's on-disk layout - and prints a
|
||||
// pass/fail checklist. The point is surfacing a broken setup as a short,
|
||||
// readable report instead of a wine-specific error buried deep inside a
|
||||
// build (see internal/wineenv.FindWine's own error, which this reuses).
|
||||
func runDoctor(args []string) int {
|
||||
if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") {
|
||||
fmt.Fprintln(os.Stderr, i18n.T("doctor.usage"))
|
||||
return 1
|
||||
}
|
||||
|
||||
d := &doctorReport{}
|
||||
d.checkWine()
|
||||
d.checkExtractionTools()
|
||||
d.checkToolchains()
|
||||
|
||||
if d.failed {
|
||||
fmt.Println(i18n.T("doctor.summary_fail"))
|
||||
return 1
|
||||
}
|
||||
fmt.Println(i18n.T("doctor.summary_ok"))
|
||||
return 0
|
||||
}
|
||||
|
||||
type doctorReport struct {
|
||||
failed bool
|
||||
}
|
||||
|
||||
func (d *doctorReport) ok(format string, args ...any) {
|
||||
fmt.Printf(" [ok] "+format+"\n", args...)
|
||||
}
|
||||
|
||||
func (d *doctorReport) warn(format string, args ...any) {
|
||||
fmt.Printf(" [warn] "+format+"\n", args...)
|
||||
}
|
||||
|
||||
func (d *doctorReport) fail(format string, args ...any) {
|
||||
fmt.Printf(" [FAIL] "+format+"\n", args...)
|
||||
d.failed = true
|
||||
}
|
||||
|
||||
func (d *doctorReport) checkWine() {
|
||||
fmt.Println(i18n.T("doctor.section_wine"))
|
||||
|
||||
wineBin, err := wineenv.FindWine()
|
||||
if err != nil {
|
||||
d.fail("%s", err)
|
||||
return
|
||||
}
|
||||
d.ok("found: %s", wineBin)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, wineBin, "--version").Output()
|
||||
if err != nil {
|
||||
d.fail("%s --version failed: %v", wineBin, err)
|
||||
return
|
||||
}
|
||||
d.ok("runs: %s", trimNewline(string(out)))
|
||||
}
|
||||
|
||||
func (d *doctorReport) checkExtractionTools() {
|
||||
fmt.Println(i18n.T("doctor.section_extract"))
|
||||
|
||||
if _, err := exec.LookPath("msiextract"); err != nil {
|
||||
d.warn(i18n.T("doctor.msitools_missing"))
|
||||
} else {
|
||||
d.ok("msitools: found (needed by `vintner download`)")
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("cabextract"); err != nil {
|
||||
d.warn(i18n.T("doctor.cabextract_missing"))
|
||||
} else {
|
||||
d.ok("cabextract: found (needed by --with-wdk/--with-dxsdk)")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *doctorReport) checkToolchains() {
|
||||
fmt.Println(i18n.T("doctor.section_toolchain"))
|
||||
|
||||
if binDir := os.Getenv("VINTNER_BIN"); binDir != "" {
|
||||
d.checkToolchainAt(binDir, "VINTNER_BIN="+binDir)
|
||||
return
|
||||
}
|
||||
|
||||
def, err := defaultToolchainDir()
|
||||
if err != nil {
|
||||
d.fail("%s", err)
|
||||
return
|
||||
}
|
||||
destBin := filepath.Join(def, "bin")
|
||||
archDirs := installedArchDirs(destBin)
|
||||
if len(archDirs) == 0 {
|
||||
d.fail(i18n.T("doctor.no_toolchain", destBin))
|
||||
return
|
||||
}
|
||||
for _, arch := range archDirs {
|
||||
d.checkToolchainAt(filepath.Join(destBin, arch), arch)
|
||||
}
|
||||
}
|
||||
|
||||
// installedArchDirs returns the subdirectories of destBin that carry their
|
||||
// own env.json, i.e. every architecture `vintner install` actually set up
|
||||
// (there can be more than one - e.g. x86 and x64 side by side).
|
||||
func installedArchDirs(destBin string) []string {
|
||||
entries, err := os.ReadDir(destBin)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var dirs []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(destBin, e.Name(), wineenv.ConfigFileName)); err == nil {
|
||||
dirs = append(dirs, e.Name())
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
func (d *doctorReport) checkToolchainAt(binDir, label string) {
|
||||
cfg, err := wineenv.Load(binDir)
|
||||
if err != nil {
|
||||
d.fail("%s: %s", label, err)
|
||||
return
|
||||
}
|
||||
baseUnix, err := wineenv.FindBaseUnix(binDir)
|
||||
if err != nil {
|
||||
d.fail("%s: %s", label, err)
|
||||
return
|
||||
}
|
||||
d.ok("%s: MSVC %s, SDK %s, root %s", label, cfg.MSVCVer, cfg.SDKVer, baseUnix)
|
||||
|
||||
paths := wineenv.NewPaths(cfg, baseUnix)
|
||||
for _, dir := range []struct{ name, path string }{
|
||||
{"MSVC bin", paths.BinDir},
|
||||
{"SDK bin", paths.SDKBinDir},
|
||||
{"MSBuild bin", paths.MSBuildBinDir},
|
||||
} {
|
||||
if fi, err := os.Stat(dir.path); err != nil || !fi.IsDir() {
|
||||
d.fail("%s: %s missing: %s", label, dir.name, dir.path)
|
||||
} else {
|
||||
d.ok("%s: %s: %s", label, dir.name, dir.path)
|
||||
}
|
||||
}
|
||||
|
||||
relay := filepath.Join(baseUnix, "bin", "toolrelay.exe")
|
||||
if fi, err := os.Stat(relay); err != nil || fi.IsDir() {
|
||||
d.warn("%s: toolrelay.exe not built (mt.exe's CMake exit-code translation won't apply; re-run `vintner install` to retry)", label)
|
||||
} else {
|
||||
d.ok("%s: toolrelay.exe: %s", label, relay)
|
||||
}
|
||||
}
|
||||
|
||||
func trimNewline(s string) string {
|
||||
for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r') {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wineenv"
|
||||
)
|
||||
|
||||
func TestRunDoctorUsage(t *testing.T) {
|
||||
for _, flag := range []string{"-h", "--help"} {
|
||||
if code := runDoctor([]string{flag}); code != 1 {
|
||||
t.Errorf("runDoctor([%q]) = %d, want 1", flag, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorReportOkWarnFail(t *testing.T) {
|
||||
d := &doctorReport{}
|
||||
d.ok("fine")
|
||||
d.warn("meh")
|
||||
if d.failed {
|
||||
t.Fatal("ok/warn must not mark the report as failed")
|
||||
}
|
||||
d.fail("broken")
|
||||
if !d.failed {
|
||||
t.Fatal("fail must mark the report as failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstalledArchDirsFindsOnlyDirsWithEnvJSON(t *testing.T) {
|
||||
destBin := t.TempDir()
|
||||
for _, dir := range []string{"x64", "x86", "not-a-toolchain"} {
|
||||
if err := os.MkdirAll(filepath.Join(destBin, dir), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, dir := range []string{"x64", "x86"} {
|
||||
if err := os.WriteFile(filepath.Join(destBin, dir, wineenv.ConfigFileName), []byte("{}"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
got := installedArchDirs(destBin)
|
||||
want := map[string]bool{"x64": true, "x86": true}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("installedArchDirs = %v, want exactly %v", got, want)
|
||||
}
|
||||
for _, arch := range got {
|
||||
if !want[arch] {
|
||||
t.Errorf("installedArchDirs returned unexpected entry %q", arch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstalledArchDirsMissingDir(t *testing.T) {
|
||||
if got := installedArchDirs(filepath.Join(t.TempDir(), "does-not-exist")); got != nil {
|
||||
t.Errorf("installedArchDirs on a missing dir = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckToolchainAtMissingEnvJSON(t *testing.T) {
|
||||
d := &doctorReport{}
|
||||
d.checkToolchainAt(t.TempDir(), "test")
|
||||
if !d.failed {
|
||||
t.Error("checkToolchainAt against a dir with no env.json should fail the report")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimNewline(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"wine-9.0\n": "wine-9.0",
|
||||
"wine-9.0\r\n": "wine-9.0",
|
||||
"wine-9.0": "wine-9.0",
|
||||
"": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := trimNewline(in); got != want {
|
||||
t.Errorf("trimNewline(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,8 @@ func runCLI(args []string) int {
|
||||
return runEnv(args[1:])
|
||||
case "completion":
|
||||
return runCompletion(args[1:])
|
||||
case "doctor":
|
||||
return runDoctor(args[1:])
|
||||
case "version", "v", "--version":
|
||||
fmt.Println(versionString())
|
||||
return 0
|
||||
|
||||
@@ -73,6 +73,7 @@ Usage:
|
||||
vintner install (i) [dir] wire up wrappers for a downloaded MSVC
|
||||
vintner env (e) --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
|
||||
vintner version (v) print the version
|
||||
vintner doctor check wine/toolchain setup
|
||||
vintner help (h) show this message
|
||||
vintner completion bash|zsh print a shell completion script
|
||||
|
||||
@@ -98,6 +99,7 @@ not the default ~/.vintner/bin/<host-arch>):
|
||||
vintner install (i) [каталог] настроить обёртки для скачанного MSVC
|
||||
vintner env (e) --bin <dir/bin/arch> вывести INCLUDE/LIB для clang-cl/lld-link напрямую
|
||||
vintner version (v) показать версию
|
||||
vintner doctor проверить настройку wine/toolchain
|
||||
vintner help (h) показать эту справку
|
||||
vintner completion bash|zsh вывести скрипт автодополнения для оболочки
|
||||
|
||||
@@ -188,4 +190,41 @@ not the default ~/.vintner/bin/<host-arch>):
|
||||
EN: "Do you accept the license? Answer \"yes\" or \"no\": ",
|
||||
RU: "Вы принимаете лицензию? Ответьте «yes» или «no»: ",
|
||||
},
|
||||
|
||||
"doctor.usage": {
|
||||
EN: "usage: vintner doctor",
|
||||
RU: "использование: vintner doctor",
|
||||
},
|
||||
"doctor.section_wine": {
|
||||
EN: "Wine:",
|
||||
RU: "Wine:",
|
||||
},
|
||||
"doctor.section_extract": {
|
||||
EN: "Extraction tools:",
|
||||
RU: "Инструменты распаковки:",
|
||||
},
|
||||
"doctor.section_toolchain": {
|
||||
EN: "Installed toolchain:",
|
||||
RU: "Установленный набор инструментов:",
|
||||
},
|
||||
"doctor.msitools_missing": {
|
||||
EN: "msitools: not found (install the msitools package - needed by `vintner download`)",
|
||||
RU: "msitools: не найден (установите пакет msitools — нужен для `vintner download`)",
|
||||
},
|
||||
"doctor.cabextract_missing": {
|
||||
EN: "cabextract: not found (install the cabextract package - only needed for --with-wdk/--with-dxsdk)",
|
||||
RU: "cabextract: не найден (установите пакет cabextract — нужен только для --with-wdk/--with-dxsdk)",
|
||||
},
|
||||
"doctor.no_toolchain": {
|
||||
EN: "no installed toolchain found under %s (run `vintner download --accept-license && vintner install` first, or set VINTNER_BIN)",
|
||||
RU: "установленный набор инструментов не найден в %s (сначала выполните `vintner download --accept-license && vintner install`, либо задайте VINTNER_BIN)",
|
||||
},
|
||||
"doctor.summary_ok": {
|
||||
EN: "\nAll checks passed.",
|
||||
RU: "\nВсе проверки пройдены.",
|
||||
},
|
||||
"doctor.summary_fail": {
|
||||
EN: "\nSome checks failed - see [FAIL] lines above.",
|
||||
RU: "\nНекоторые проверки не пройдены — см. строки [FAIL] выше.",
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user