mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
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:
@@ -1,6 +1,11 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wrapper"
|
||||
)
|
||||
|
||||
// runCompletion prints a shell completion script for shell ("bash" or
|
||||
// "zsh") to stdout, meant to be sourced directly:
|
||||
@@ -11,6 +16,11 @@ import "fmt"
|
||||
// The flag lists below are hand-maintained alongside download.go/env.go's
|
||||
// flag.FlagSet definitions rather than generated from them - there's no
|
||||
// reflection-friendly registry to walk, and the flag set rarely changes.
|
||||
// The tool name list isn't hand-maintained, though (see wrapper.ToolNames):
|
||||
// a hand-copied one already went stale once already for a flag
|
||||
// (--with-dxsdk missing from here after being added to download.go), and a
|
||||
// list of every wrapped tool has more entries and changes for the same
|
||||
// reasons the flag lists do, so it's worth generating for real.
|
||||
func runCompletion(args []string) int {
|
||||
if len(args) != 1 {
|
||||
fmt.Println("usage: vintner completion bash|zsh")
|
||||
@@ -18,10 +28,10 @@ func runCompletion(args []string) int {
|
||||
}
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
fmt.Print(bashCompletionScript)
|
||||
fmt.Print(bashCompletionScript())
|
||||
return 0
|
||||
case "zsh":
|
||||
fmt.Print(zshCompletionScript)
|
||||
fmt.Print(zshCompletionScript())
|
||||
return 0
|
||||
default:
|
||||
fmt.Printf("vintner completion: unsupported shell %q (want bash or zsh)\n", args[0])
|
||||
@@ -33,9 +43,16 @@ const downloadFlags = "--dest --cache --major --preview --manifest --accept-lice
|
||||
"--msvc-version --sdk-version --host-arch --only-host --language " +
|
||||
"--include-optional --skip-recommended --only-download --only-unpack " +
|
||||
"--keep-unpack --skip-patch --list-workloads --list-components " +
|
||||
"--print-deps-tree --with-wdk --architecture --ignore -h --help"
|
||||
"--print-deps-tree --with-wdk --with-dxsdk --architecture --ignore -h --help"
|
||||
|
||||
var bashCompletionScript = `# vintner bash completion - eval "$(vintner completion bash)"
|
||||
// subcommandNames lists vintner's own management subcommands (long form
|
||||
// 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"
|
||||
|
||||
func bashCompletionScript() string {
|
||||
return `# vintner bash completion - eval "$(vintner completion bash)"
|
||||
_vintner_complete() {
|
||||
local cur cmd
|
||||
COMPREPLY=()
|
||||
@@ -43,7 +60,7 @@ _vintner_complete() {
|
||||
cmd="${COMP_WORDS[1]}"
|
||||
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
COMPREPLY=($(compgen -W "download dl install i env e version v help h completion" -- "$cur"))
|
||||
COMPREPLY=($(compgen -W "` + subcommandNames + ` ` + strings.Join(wrapper.ToolNames(), " ") + `" -- "$cur"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -65,8 +82,15 @@ _vintner_complete() {
|
||||
}
|
||||
complete -F _vintner_complete vintner
|
||||
`
|
||||
}
|
||||
|
||||
var zshCompletionScript = `#compdef vintner
|
||||
func zshCompletionScript() string {
|
||||
var toolEntries strings.Builder
|
||||
for _, name := range wrapper.ToolNames() {
|
||||
fmt.Fprintf(&toolEntries, " %q\n", name+":run this tool directly, e.g. \"vintner "+name+" ...\"")
|
||||
}
|
||||
|
||||
return `#compdef vintner
|
||||
# vintner zsh completion - source <(vintner completion zsh)
|
||||
|
||||
_vintner() {
|
||||
@@ -83,7 +107,7 @@ _vintner() {
|
||||
'help:print usage'
|
||||
'h:alias for help'
|
||||
'completion:print a shell completion script'
|
||||
)
|
||||
` + toolEntries.String() + ` )
|
||||
|
||||
if (( CURRENT == 2 )); then
|
||||
_describe 'command' subcommands
|
||||
@@ -115,6 +139,7 @@ _vintner() {
|
||||
'--list-components[list available components and exit]'
|
||||
'--print-deps-tree[print the dependency tree and exit]'
|
||||
'--with-wdk[also fetch the Windows Driver Kit]'
|
||||
'--with-dxsdk[also fetch the DirectX SDK]'
|
||||
'--architecture[target architecture]:arch:(x86 x64 arm arm64 host)'
|
||||
'--ignore[package id to skip]:package id:'
|
||||
'-h[show help]'
|
||||
@@ -138,3 +163,4 @@ _vintner() {
|
||||
|
||||
_vintner "$@"
|
||||
`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wrapper"
|
||||
)
|
||||
|
||||
// TestCompletionScriptsAreSyntacticallyValid catches the easy way to break
|
||||
@@ -15,8 +17,8 @@ func TestCompletionScriptsAreSyntacticallyValid(t *testing.T) {
|
||||
shell string
|
||||
script string
|
||||
}{
|
||||
{"bash", bashCompletionScript},
|
||||
{"zsh", zshCompletionScript},
|
||||
{"bash", bashCompletionScript()},
|
||||
{"zsh", zshCompletionScript()},
|
||||
} {
|
||||
t.Run(tc.shell, func(t *testing.T) {
|
||||
if _, err := exec.LookPath(tc.shell); err != nil {
|
||||
@@ -31,6 +33,24 @@ func TestCompletionScriptsAreSyntacticallyValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompletionScriptsListEveryTool guards against the exact staleness bug
|
||||
// found and fixed alongside this test: a hand-copied tool/flag list here
|
||||
// drifting from the real set in internal/wrapper (or download.go's flags)
|
||||
// as tools/flags get added. Every current tool name must appear in both
|
||||
// generated scripts.
|
||||
func TestCompletionScriptsListEveryTool(t *testing.T) {
|
||||
bash := bashCompletionScript()
|
||||
zsh := zshCompletionScript()
|
||||
for _, name := range wrapper.ToolNames() {
|
||||
if !strings.Contains(bash, name) {
|
||||
t.Errorf("bash completion script doesn't mention tool %q", name)
|
||||
}
|
||||
if !strings.Contains(zsh, name+":") {
|
||||
t.Errorf("zsh completion script doesn't mention tool %q", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCompletionUnknownShell(t *testing.T) {
|
||||
if code := runCompletion([]string{"fish"}); code != 1 {
|
||||
t.Errorf("runCompletion([\"fish\"]) = %d, want 1", code)
|
||||
|
||||
+9
-8
@@ -1,9 +1,9 @@
|
||||
// Command vintner cross compiles with the real MSVC toolchain on Linux
|
||||
// via Wine. It's a multi-call binary that behaves as `cl`, `link`, `lib`,
|
||||
// `rc`, `midl`, `mt`, `dumpbin`, `msbuild`, etc. when invoked under one of
|
||||
// those names (via symlinks set up by `vintner install`), and
|
||||
// otherwise exposes the `download`/`install`/`env`/`version` management
|
||||
// subcommands.
|
||||
// those names (via symlinks set up by `vintner install`) or as
|
||||
// `vintner <tool> ...` directly (see runTool), and otherwise exposes the
|
||||
// `download`/`install`/`env`/`version` management subcommands.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -24,11 +24,8 @@ func main() {
|
||||
base := filepath.Base(os.Args[0])
|
||||
name := strings.TrimSuffix(strings.ToLower(base), ".exe")
|
||||
|
||||
if _, ok := wrapper.Tools[name]; ok {
|
||||
os.Exit(wrapper.Run(name, os.Args[1:]))
|
||||
}
|
||||
if name == "cmd" || name == "findstr" {
|
||||
os.Exit(wrapper.Run(name, os.Args[1:]))
|
||||
if wrapper.IsTool(name) {
|
||||
os.Exit(wrapper.Run(name, os.Args[1:], ""))
|
||||
}
|
||||
|
||||
os.Exit(runCLI(os.Args[1:]))
|
||||
@@ -40,6 +37,10 @@ func runCLI(args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
if wrapper.IsTool(args[0]) {
|
||||
return runTool(args[0], args[1:])
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "download", "dl":
|
||||
return runDownload(args[1:])
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Cheviiot/vintner/internal/wrapper"
|
||||
)
|
||||
|
||||
// runTool dispatches `vintner <tool> [args...]` (cl, link, msbuild, ...)
|
||||
// directly, without needing <dest>/bin/<arch> on PATH or a same-directory
|
||||
// symlink pointing back at this binary.
|
||||
//
|
||||
// Resolves which toolchain bin dir to use from VINTNER_BIN if set (same
|
||||
// meaning as `env --bin`: point it directly at a <dest>/bin/<arch>
|
||||
// directory - useful for a non-default --dest, or to pick a specific
|
||||
// architecture when more than one is installed), else defaults to
|
||||
// <defaultToolchainDir>/bin/<hostArch>, the layout a plain `vintner
|
||||
// download && vintner install` with no --dest override produces.
|
||||
func runTool(tool string, args []string) int {
|
||||
binDir := os.Getenv("VINTNER_BIN")
|
||||
if binDir == "" {
|
||||
def, err := defaultToolchainDir()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "vintner:", err)
|
||||
return 1
|
||||
}
|
||||
binDir = filepath.Join(def, "bin", detectHostArch())
|
||||
}
|
||||
if fi, err := os.Stat(binDir); err != nil || !fi.IsDir() {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"vintner: no installed toolchain found at %s\n"+
|
||||
"Run `vintner download --accept-license && vintner install` first, "+
|
||||
"or set VINTNER_BIN to an existing <dest>/bin/<arch> directory.\n",
|
||||
binDir)
|
||||
return 1
|
||||
}
|
||||
return wrapper.Run(tool, args, binDir)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunToolReportsMissingToolchain(t *testing.T) {
|
||||
t.Setenv("VINTNER_BIN", filepath.Join(t.TempDir(), "does-not-exist"))
|
||||
if code := runTool("cl", nil); code != 1 {
|
||||
t.Errorf("runTool with a nonexistent VINTNER_BIN = %d, want 1", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunToolUsesVINTNERBinOverDefault(t *testing.T) {
|
||||
// A directory that exists but has no env.json - past the "toolchain
|
||||
// found at all" check, into wrapper.Run's own (already-tested)
|
||||
// env.json-loading error path. Confirms VINTNER_BIN is actually being
|
||||
// read and passed through, without needing a full fake toolchain.
|
||||
t.Setenv("VINTNER_BIN", t.TempDir())
|
||||
if code := runTool("cl", nil); code != 1 {
|
||||
t.Errorf("runTool with an empty VINTNER_BIN dir = %d, want 1 (from the missing env.json)", code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user