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:
@@ -21,6 +21,7 @@ approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
||||
- [Installation](#installation)
|
||||
- [Quick start](#quick-start)
|
||||
- [Commands](#commands)
|
||||
- [Invoking tools without PATH](#invoking-tools-without-path)
|
||||
- [Building drivers (WDK)](#building-drivers-wdk)
|
||||
- [Building against D3DX9 (DirectX SDK)](#building-against-d3dx9-directx-sdk)
|
||||
- [Automated/scripted builds](#automatedscripted-builds)
|
||||
@@ -102,6 +103,9 @@ export PATH=~/.vintner/bin/x64:$PATH
|
||||
cl /nologo /EHsc hello.cpp
|
||||
```
|
||||
|
||||
Don't want to touch PATH? Skip step 3 and run tools through vintner
|
||||
directly instead - see [Invoking tools without PATH](#invoking-tools-without-path).
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
@@ -132,6 +136,24 @@ package id or through `--with-*`. `--print-deps-tree` prints the
|
||||
dependency tree of whatever would actually be selected — honoring every
|
||||
other flag — without downloading anything.
|
||||
|
||||
## Invoking tools without PATH
|
||||
|
||||
`cl`, `link`, `msbuild`, and the rest also work as `vintner <tool>
|
||||
[args...]`, with no need to add `<dest>/bin/<arch>` to `PATH` or rely on
|
||||
the symlinks `install` sets up there:
|
||||
|
||||
```bash
|
||||
vintner cl /nologo /EHsc hello.cpp
|
||||
vintner msbuild MyProject.sln
|
||||
```
|
||||
|
||||
Resolves the toolchain to use from `VINTNER_BIN` if set (same meaning as
|
||||
`env --bin`: point it at a `<dest>/bin/<arch>` directory directly — useful
|
||||
for a non-default `--dest`, or to pick a specific architecture when more
|
||||
than one is installed), otherwise defaults to
|
||||
`~/.vintner/bin/<host-arch>`, the layout a plain `vintner download &&
|
||||
vintner install` with no `--dest` override produces.
|
||||
|
||||
## Building drivers (WDK)
|
||||
|
||||
`--with-wdk` also fetches the Windows Driver Kit: headers, import libs,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -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": {
|
||||
|
||||
+20
-8
@@ -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
|
||||
}
|
||||
|
||||
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.
|
||||
// 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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user