Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/aima/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 34 additions & 0 deletions internal/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
149 changes: 64 additions & 85 deletions internal/engine/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
Loading