From 876a5c7c2692151d235e3d633aa6be7e050e04a1 Mon Sep 17 00:00:00 2001 From: rjckkkkk <59609580+rjckkkkk@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:30:39 +0000 Subject: [PATCH] Discover off-PATH native engine binaries via AIMA_ENGINE_DIR Engine scan only looked in ~/.aima/dist/{os-arch}/ and PATH for native engine binaries, so engines installed in arbitrary dirs off the system drive (e.g. Windows D:\tools\llama-b9180-win-hip-radeon-x64\llama-server.exe) were invisible and `aima engine scan` returned []. Add ScanOptions.ExtraDirs, fed from a new AIMA_ENGINE_DIR OS-path-list env var (mirrors AIMA_MODEL_DIR), and extract scanDirForEngineBinaries so distDir / extra dirs / PATH share one scan path with consistent dedup and ID semantics. Once discovered, the existing local-engine overlay reinjects the binary path as a preinstalled probe, so native deploy resolves the absolute binary with no further changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmd/aima/main.go | 10 +++ internal/engine/engine_test.go | 34 ++++++++ internal/engine/scanner.go | 149 ++++++++++++++------------------- 3 files changed, 108 insertions(+), 85 deletions(-) diff --git a/cmd/aima/main.go b/cmd/aima/main.go index 1f4f7bab..bbd1445c 100644 --- a/cmd/aima/main.go +++ b/cmd/aima/main.go @@ -1145,10 +1145,20 @@ func buildToolDeps(ac *appContext) *mcp.ToolDeps { } platform := goruntime.GOOS + "-" + goruntime.GOARCH distDir := filepath.Join(dataDir, "dist", platform) + // AIMA_ENGINE_DIR lists dirs holding pre-installed engine binaries that + // live off PATH and off dist (e.g. Windows D:\tools\llama-b9180-...\). + // Accepts an OS path list, mirroring AIMA_MODEL_DIR. + var engineExtraDirs []string + for _, dir := range filepath.SplitList(os.Getenv("AIMA_ENGINE_DIR")) { + if dir = strings.TrimSpace(dir); dir != "" { + engineExtraDirs = append(engineExtraDirs, dir) + } + } images, err := engine.ScanUnified(ctx, engine.ScanOptions{ AssetPatterns: assetPatterns, Runner: &execRunner{}, DistDir: distDir, + ExtraDirs: engineExtraDirs, Platform: platform, BinaryAssets: binaryAssets, AutoImport: autoImport, diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 3946fe3b..399eac65 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -787,6 +787,40 @@ func TestScanPreinstalledProbeUsesDiscoveredBinaryPath(t *testing.T) { } } +func TestScanNativeFindsBinaryInExtraDirs(t *testing.T) { + // Engines installed off the system drive in arbitrary dirs (e.g. Windows + // D:\tools\llama-b9180-win-hip-radeon-x64) are neither in distDir nor on + // PATH. AIMA_ENGINE_DIR feeds those dirs in via ScanOptions.ExtraDirs. + distDir := t.TempDir() + engineDir := t.TempDir() + binPath := filepath.Join(engineDir, "llama-server.exe") + if err := os.WriteFile(binPath, []byte("stub"), 0o755); err != nil { + t.Fatalf("write fake binary: %v", err) + } + + results, err := ScanNative(context.Background(), ScanOptions{ + DistDir: distDir, + Platform: "windows-amd64", + BinaryAssets: map[string]string{"llama-server": "llamacpp", "llama-server.exe": "llamacpp"}, + ExtraDirs: []string{engineDir}, + }) + if err != nil { + t.Fatalf("scan native: %v", err) + } + if len(results) != 1 { + t.Fatalf("expected 1 native engine, got %d", len(results)) + } + if got := results[0].Type; got != "llamacpp" { + t.Errorf("Type = %q, want llamacpp", got) + } + if got := results[0].BinaryPath; got != binPath { + t.Errorf("BinaryPath = %q, want %q", got, binPath) + } + if got := results[0].RuntimeType; got != "native" { + t.Errorf("RuntimeType = %q, want native", got) + } +} + func TestPullImageNameConstruction(t *testing.T) { // Verify image refs are built correctly for host-only registries, namespace // prefixes, and fully-qualified repository overrides. diff --git a/internal/engine/scanner.go b/internal/engine/scanner.go index 58f0840e..fe916133 100644 --- a/internal/engine/scanner.go +++ b/internal/engine/scanner.go @@ -49,6 +49,7 @@ type ScanOptions struct { AssetPatterns map[string][]string // engine type -> patterns from Engine Asset YAML Runner CommandRunner DistDir string // dist directory for native binaries (~/.aima/dist/{os}-{arch}/) + ExtraDirs []string // extra dirs to scan for native binaries (from AIMA_ENGINE_DIR); engines installed off-PATH/off-dist Platform string // current platform (e.g., "windows-amd64") BinaryAssets map[string]string // binary name -> engine type (native engines) AutoImport bool // when true, auto-import Docker-only images to K3S containerd (heavy; use only during init) @@ -128,7 +129,10 @@ func ScanUnified(ctx context.Context, opts ScanOptions) ([]*EngineImage, error) return allEngines, nil } -// ScanNative discovers native engine binaries in distDir and PATH. +// ScanNative discovers native engine binaries in distDir, the AIMA_ENGINE_DIR +// extra dirs, and PATH. Engines installed off the system drive in arbitrary +// dirs (e.g. D:\tools\llama-b9180-win-hip-radeon-x64\llama-server.exe) are +// neither in distDir nor on PATH, so ExtraDirs is the path for those. func ScanNative(ctx context.Context, opts ScanOptions) ([]*EngineImage, error) { if opts.DistDir == "" { return nil, fmt.Errorf("distDir not configured") @@ -150,99 +154,74 @@ func ScanNative(ctx context.Context, opts ScanOptions) ([]*EngineImage, error) { var found []*EngineImage seen := make(map[string]bool) - // Scan distDir - if entries, err := os.ReadDir(opts.DistDir); err == nil { - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - if seen[name] { - continue - } - // Check if this is a known engine binary (with or without .exe) - binaryName := name - if strings.HasSuffix(name, ".exe") { - binaryName = strings.TrimSuffix(name, ".exe") - } - engineType, ok1 := filenameLookup[binaryName] - if !ok1 { - engineType, ok1 = filenameLookup[name] - } - if ok1 { - path := filepath.Join(opts.DistDir, name) - info, err := entry.Info() - if err != nil { - continue - } - binaryID := binaryHash(name) - found = append(found, &EngineImage{ - ID: binaryID, - Type: engineType, - Image: "", - Tag: "", - SizeBytes: info.Size(), - Platform: opts.Platform, - RuntimeType: "native", - BinaryPath: path, - Available: true, - }) - seen[name] = true - } - } + // distDir holds AIMA's own managed copy → dir-independent ID. Extra dirs and + // PATH dirs may hold the same binary name in different locations → salt the ID + // with the dir so they stay distinct. First match for a filename wins (seen). + found = append(found, scanDirForEngineBinaries(opts.DistDir, filenameLookup, seen, opts.Platform, false)...) + for _, dir := range opts.ExtraDirs { + found = append(found, scanDirForEngineBinaries(dir, filenameLookup, seen, opts.Platform, true)...) } - - // Scan PATH for additional binaries - pathEnv := os.Getenv("PATH") - if pathEnv != "" { + if pathEnv := os.Getenv("PATH"); pathEnv != "" { sep := string(os.PathListSeparator) for _, dir := range strings.Split(pathEnv, sep) { - if entries, err := os.ReadDir(dir); err == nil { - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - if seen[name] { - continue - } - // Check if this is a known engine binary - binaryName := name - if strings.HasSuffix(name, ".exe") { - binaryName = strings.TrimSuffix(name, ".exe") - } - engineType, ok1 := filenameLookup[binaryName] - if !ok1 { - engineType, ok1 = filenameLookup[name] - } - if ok1 { - path := filepath.Join(dir, name) - info, err := entry.Info() - if err != nil { - continue - } - binaryID := binaryHash(name + "-" + dir) - found = append(found, &EngineImage{ - ID: binaryID, - Type: engineType, - Image: "", - Tag: "", - SizeBytes: info.Size(), - Platform: opts.Platform, - RuntimeType: "native", - BinaryPath: path, - Available: true, - }) - seen[name] = true - } - } - } + found = append(found, scanDirForEngineBinaries(dir, filenameLookup, seen, opts.Platform, true)...) } } return found, nil } +// scanDirForEngineBinaries returns native engines for known binaries located +// directly in dir (never recurses). saltID controls whether the engine ID is +// salted with the dir, so the same binary name in multiple dirs yields distinct +// IDs. Binaries whose filename is already in seen are skipped (first dir wins). +func scanDirForEngineBinaries(dir string, filenameLookup map[string]string, seen map[string]bool, platform string, saltID bool) []*EngineImage { + if dir == "" { + return nil + } + entries, err := os.ReadDir(dir) + if err != nil { + return nil + } + var found []*EngineImage + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if seen[name] { + continue + } + // Match known engine binary with or without .exe suffix. + engineType, ok := filenameLookup[strings.TrimSuffix(name, ".exe")] + if !ok { + engineType, ok = filenameLookup[name] + } + if !ok { + continue + } + info, err := entry.Info() + if err != nil { + continue + } + id := binaryHash(name) + if saltID { + id = binaryHash(name + "-" + dir) + } + found = append(found, &EngineImage{ + ID: id, + Type: engineType, + SizeBytes: info.Size(), + Platform: platform, + RuntimeType: "native", + BinaryPath: filepath.Join(dir, name), + Available: true, + }) + seen[name] = true + } + return found +} + // probePreinstalled discovers pre-installed engines by checking known paths // and optionally running version detection commands. func probePreinstalled(ctx context.Context, opts ScanOptions) []*EngineImage {