Run wrapped tools directly as vintner <tool> ..., no PATH needed

`vintner cl ...`, `vintner msbuild ...`, etc. now work without adding
<dest>/bin/<arch> to PATH or relying on the same-directory symlinks
`install` sets up there. wrapper.Run gained an explicit binDir
parameter (empty string preserves the existing os.Executable()-based
self-location for the ordinary multi-call/symlink case) so
cmd/vintner's new runTool can point it at a resolved toolchain
directory instead.

Resolution order: VINTNER_BIN if set (same meaning as `env --bin` -
point it at a <dest>/bin/<arch> directory directly, for a non-default
--dest or a specific architecture), else <defaultToolchainDir>/bin/
<hostArch> - the layout a plain `vintner download && vintner install`
with no --dest override produces. A missing toolchain gets a clear
error pointing at both fixes, rather than bubbling up whatever
wineenv.Load's env.json error looks like.

Also fixed shell completion falling out of sync with the actual tool
list: the bash/zsh scripts previously hand-copied tool/flag names
(and had already gone stale once - --with-dxsdk was missing from the
download flag completions since it was added). The tool name list is
now generated from wrapper.ToolNames() instead of hand-maintained,
and both scripts now complete tool names too, so `vintner <TAB>`
suggests `cl`, `link`, `msbuild`, etc. alongside the management
subcommands.

Verified end-to-end with a minimal PATH (/usr/bin:/bin only, no
toolchain dir on it at all): `vintner cl /nologo hello.c` compiled
successfully, and `vintner msbuild -t:Rebuild ...` rebuilt the same
real KMDF driver verified earlier this session - both via the default
~/.vintner/bin/<hostArch> resolution, no VINTNER_BIN override needed.
This commit is contained in:
Cheviiot
2026-07-25 16:23:46 +10:00
parent f6b9a0811a
commit 738234186d
10 changed files with 236 additions and 36 deletions
+11 -4
View File
@@ -84,7 +84,10 @@ has many, including --with-wdk, --with-dxsdk, --list-workloads,
Language: set VINTNER_LANG=ru (or LANG=ru_RU...) for Russian output.
Completion: source <(vintner completion bash) # or zsh
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly,
or skip PATH and run them through vintner itself - "vintner cl ...",
"vintner msbuild ...", etc. (set VINTNER_BIN to a <dir>/bin/<arch> if it's
not the default ~/.vintner/bin/<host-arch>):
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
`,
RU: `vintner — кросс-компиляция настоящим MSVC на Linux через Wine
@@ -104,9 +107,13 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
--dest/[каталог] по умолчанию — ~/.vintner.
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
Можно не трогать PATH: "vintner cl ...", "vintner msbuild ..." и т.д. работают
напрямую (VINTNER_BIN — если каталог не стандартный ~/.vintner/bin/<hostarch>).
Автодополнение: source <(vintner completion bash) # или zsh
После установки добавьте <dir>/bin/<arch> в PATH и вызывайте инструменты напрямую:
После установки добавьте <dir>/bin/<arch> в PATH и вызывайте инструменты
напрямую, либо не трогая PATH — «vintner cl ...», «vintner msbuild ...»
и т.д.:
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
`,
},
@@ -124,8 +131,8 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
RU: "Каталог не указан, используется значение по умолчанию: %s",
},
"install.done": {
EN: "Done. Add %s to PATH to use cl, link, lib, ...",
RU: "Готово. Добавьте %s в PATH, чтобы использовать cl, link, lib и т.д.",
EN: "Done. Add %s to PATH to use cl, link, lib, ... directly, or run them as \"vintner cl\", \"vintner link\", etc. without touching PATH.",
RU: "Готово. Добавьте %s в PATH, чтобы использовать cl, link, lib и т.д. напрямую, либо запускайте их как «vintner cl», «vintner link» и т.д., не трогая PATH.",
},
"env.usage": {
+25 -13
View File
@@ -30,7 +30,15 @@ const toolRelayName = "toolrelay.exe"
// Run executes the named multi-call tool with args, exactly as the original
// bash wrappers would, and returns the process exit code.
func Run(tool string, args []string) int {
//
// binDir is the <dest>/bin/<arch> directory holding env.json for this
// invocation. Pass "" for the ordinary multi-call case (invoked as `cl`,
// `link`, etc. via a same-directory symlink) to have it resolved from the
// running binary's own location; a non-empty value is for `vintner <tool>
// ...` direct dispatch (see cmd/vintner's runTool), which isn't running
// from inside any particular <dest>/bin/<arch> and so has nothing to
// resolve on its own.
func Run(tool string, args []string, binDir string) int {
if nativeTools[tool] {
return runNative(tool, args)
}
@@ -41,19 +49,23 @@ func Run(tool string, args []string) int {
return 127
}
// os.Executable() (backed by /proc/self/exe on Linux) fully resolves
// symlinks, unlike os.Args[0]: not every shell passes a PATH-resolved
// absolute path as argv[0] (some just pass the bare command name), which
// would make an argv[0]-based lookup resolve against the caller's cwd
// instead of the actual install dir. `install` sets each arch dir up
// with its own local copy of the binary precisely so this resolves to
// <dest>/bin/<arch>, not <dest>/bin.
exePath, err := os.Executable()
if err != nil {
fmt.Fprintln(os.Stderr, "vintner:", err)
return 1
scriptDir := binDir
if scriptDir == "" {
// os.Executable() (backed by /proc/self/exe on Linux) fully resolves
// symlinks, unlike os.Args[0]: not every shell passes a
// PATH-resolved absolute path as argv[0] (some just pass the bare
// command name), which would make an argv[0]-based lookup resolve
// against the caller's cwd instead of the actual install dir.
// `install` sets each arch dir up with its own local copy of the
// binary precisely so this resolves to <dest>/bin/<arch>, not
// <dest>/bin.
exePath, err := os.Executable()
if err != nil {
fmt.Fprintln(os.Stderr, "vintner:", err)
return 1
}
scriptDir = filepath.Dir(exePath)
}
scriptDir := filepath.Dir(exePath)
cfg, err := wineenv.Load(scriptDir)
if err != nil {
+31 -1
View File
@@ -4,7 +4,11 @@
// filtering its output.
package wrapper
import "github.com/Cheviiot/vintner/internal/wineenv"
import (
"sort"
"github.com/Cheviiot/vintner/internal/wineenv"
)
// dirKind selects which install directory a tool's real .exe lives in.
type dirKind int
@@ -46,6 +50,32 @@ var Tools = map[string]spec{
// nativeTools are handled entirely without Wine.
var nativeTools = map[string]bool{"cmd": true, "findstr": true}
// IsTool reports whether name is a recognized wrapped tool - either a
// Wine-hosted one in Tools or a native shim in nativeTools. Shared between
// the ordinary multi-call dispatch (invoked *as* one of these names via a
// same-directory symlink) and `vintner <tool> ...` direct dispatch, so both
// recognize exactly the same set of names.
func IsTool(name string) bool {
_, ok := Tools[name]
return ok || nativeTools[name]
}
// ToolNames returns every recognized tool name, sorted - Tools and
// nativeTools combined. Used to generate shell completion without a
// separate hand-maintained list that could drift from the real set (as
// happened once already with a stale --with-dxsdk completion entry).
func ToolNames() []string {
names := make([]string, 0, len(Tools)+len(nativeTools))
for name := range Tools {
names = append(names, name)
}
for name := range nativeTools {
names = append(names, name)
}
sort.Strings(names)
return names
}
func (s spec) exeDir(p *wineenv.Paths) string {
switch s.dir {
case dirSDK:
+18
View File
@@ -43,3 +43,21 @@ func TestEveryToolHasAnExeName(t *testing.T) {
}
}
}
func TestIsTool(t *testing.T) {
for name := range Tools {
if !IsTool(name) {
t.Errorf("IsTool(%q) = false, want true (it's in Tools)", name)
}
}
for name := range nativeTools {
if !IsTool(name) {
t.Errorf("IsTool(%q) = false, want true (it's in nativeTools)", name)
}
}
for _, name := range []string{"download", "install", "env", "version", "help", "completion", "frobnicate", ""} {
if IsTool(name) {
t.Errorf("IsTool(%q) = true, want false", name)
}
}
}