diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 61496656d..82ca3d975 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file ## Added +- Add `internal/libpath` package for finding native library paths on Linux with parallel search, caching, and support for Flatpak/Snap/Nix ## Changed diff --git a/v3/internal/libpath/cache_linux.go b/v3/internal/libpath/cache_linux.go new file mode 100644 index 000000000..5591c6833 --- /dev/null +++ b/v3/internal/libpath/cache_linux.go @@ -0,0 +1,79 @@ +//go:build linux + +package libpath + +import "sync" + +// pathCache holds cached dynamic library paths to avoid repeated +// expensive filesystem and subprocess operations. +type pathCache struct { + mu sync.RWMutex + flatpak []string + snap []string + nix []string + initOnce sync.Once + inited bool +} + +var cache pathCache + +// init populates the cache with dynamic paths from package managers. +// This is called lazily on first access. +func (c *pathCache) init() { + c.initOnce.Do(func() { + // Discover paths without holding the lock + flatpak := discoverFlatpakLibPaths() + snap := discoverSnapLibPaths() + nix := discoverNixLibPaths() + + // Hold lock only while updating the cache + c.mu.Lock() + c.flatpak = flatpak + c.snap = snap + c.nix = nix + c.inited = true + c.mu.Unlock() + }) +} + +// getFlatpak returns cached Flatpak library paths. +func (c *pathCache) getFlatpak() []string { + c.init() + c.mu.RLock() + defer c.mu.RUnlock() + return c.flatpak +} + +// getSnap returns cached Snap library paths. +func (c *pathCache) getSnap() []string { + c.init() + c.mu.RLock() + defer c.mu.RUnlock() + return c.snap +} + +// getNix returns cached Nix library paths. +func (c *pathCache) getNix() []string { + c.init() + c.mu.RLock() + defer c.mu.RUnlock() + return c.nix +} + +// invalidate clears the cache and forces re-discovery on next access. +func (c *pathCache) invalidate() { + c.mu.Lock() + defer c.mu.Unlock() + c.flatpak = nil + c.snap = nil + c.nix = nil + c.initOnce = sync.Once{} // Reset so init() runs again + c.inited = false +} + +// InvalidateCache clears the cached dynamic library paths. +// Call this if packages are installed or removed during runtime +// and you need to re-discover library paths. +func InvalidateCache() { + cache.invalidate() +} diff --git a/v3/internal/libpath/flatpak_linux.go b/v3/internal/libpath/flatpak_linux.go new file mode 100644 index 000000000..ce83c08c6 --- /dev/null +++ b/v3/internal/libpath/flatpak_linux.go @@ -0,0 +1,53 @@ +//go:build linux + +package libpath + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +// getFlatpakLibPaths returns cached library paths from installed Flatpak runtimes. +func getFlatpakLibPaths() []string { + return cache.getFlatpak() +} + +// discoverFlatpakLibPaths scans for Flatpak runtime library directories. +// Uses `flatpak --installations` and scans for runtime lib directories. +func discoverFlatpakLibPaths() []string { + var paths []string + + // Get system and user installation directories + installDirs := []string{ + "/var/lib/flatpak", // System default + os.ExpandEnv("$HOME/.local/share/flatpak"), // User default + } + + // Try to get actual installation path from flatpak + if out, err := exec.Command("flatpak", "--installations").Output(); err == nil { + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + if line != "" { + installDirs = append(installDirs, line) + } + } + } + + // Scan for runtime lib directories + for _, installDir := range installDirs { + runtimeDir := filepath.Join(installDir, "runtime") + if _, err := os.Stat(runtimeDir); err != nil { + continue + } + + // Look for lib directories in runtimes + // Structure: runtime/////files/lib + matches, err := filepath.Glob(filepath.Join(runtimeDir, "*", "*", "*", "*", "files", "lib")) + if err == nil { + paths = append(paths, matches...) + } + } + + return paths +} diff --git a/v3/internal/libpath/libpath.go b/v3/internal/libpath/libpath.go new file mode 100644 index 000000000..4c32ce518 --- /dev/null +++ b/v3/internal/libpath/libpath.go @@ -0,0 +1,104 @@ +// Package libpath provides utilities for finding native library paths on Linux. +// +// # Overview +// +// This package helps locate shared libraries (.so files) on Linux systems, +// supporting multiple distributions and package managers. It's particularly +// useful for applications that need to link against libraries like GTK, +// WebKit2GTK, or other system libraries at runtime. +// +// # Search Strategy +// +// The package uses a multi-tier search strategy, trying each method in order +// until a library is found: +// +// 1. pkg-config: Queries the pkg-config database for library paths +// 2. ldconfig: Searches the dynamic linker cache +// 3. Filesystem: Scans common library directories +// +// # Supported Distributions +// +// The package includes default search paths for: +// +// - Debian/Ubuntu (multiarch paths like /usr/lib/x86_64-linux-gnu) +// - Fedora/RHEL/CentOS (/usr/lib64, /usr/lib64/gtk-*) +// - Arch Linux (/usr/lib/webkit2gtk-*, /usr/lib/gtk-*) +// - openSUSE (/usr/lib64/gcc/x86_64-suse-linux) +// - NixOS and Nix package manager +// +// # Package Manager Support +// +// Dynamic paths are discovered from: +// +// - Flatpak: Scans runtime directories via `flatpak --installations` +// - Snap: Globs /snap/*/current/usr/lib* directories +// - Nix: Checks ~/.nix-profile/lib and /run/current-system/sw/lib +// +// # Caching +// +// Dynamic path discovery (Flatpak, Snap, Nix) is cached for performance. +// The cache is populated on first access and persists for the process lifetime. +// Use [InvalidateCache] to force re-discovery if packages are installed/removed +// during runtime. +// +// # Security +// +// The current directory (".") is never included in search paths by default, +// as this is a security risk. Use [FindLibraryPathWithOptions] with +// IncludeCurrentDir if you explicitly need this behavior (not recommended +// for production). +// +// # Performance +// +// Typical lookup times (cached): +// +// - Found via pkg-config: ~2ms (spawns external process) +// - Found via ldconfig: ~1.3ms (spawns external process) +// - Found via filesystem: ~0.1ms (uses cached paths) +// - Not found (worst case): ~20ms (searches all paths) +// +// # Example Usage +// +// // Find a library by its pkg-config name +// path, err := libpath.FindLibraryPath("webkit2gtk-4.1") +// if err != nil { +// log.Fatal("WebKit2GTK not found:", err) +// } +// fmt.Println("Found at:", path) +// +// // Find a specific .so file +// soPath, err := libpath.FindLibraryFile("libgtk-3.so") +// if err != nil { +// log.Fatal("GTK3 library file not found:", err) +// } +// fmt.Println("Library file:", soPath) +// +// // Get all library search paths +// for _, p := range libpath.GetAllLibPaths() { +// fmt.Println(p) +// } +// +// # Multi-Library Search +// +// When you don't know which version of a library is installed, use the +// multi-library search functions: +// +// // Find any available WebKit2GTK version (first found wins) +// match, err := libpath.FindFirstLibrary("webkit2gtk-4.1", "webkit2gtk-4.0", "webkit2gtk-6.0") +// if err != nil { +// log.Fatal("No WebKit2GTK found") +// } +// fmt.Printf("Found %s at %s\n", match.Name, match.Path) +// +// // Prefer newer versions (ordered search) +// match, err := libpath.FindFirstLibraryOrdered("gtk4", "gtk+-3.0") +// +// // Discover all available versions +// matches := libpath.FindAllLibraries("gtk+-3.0", "gtk4", "webkit2gtk-4.0", "webkit2gtk-4.1") +// for _, m := range matches { +// fmt.Printf("Available: %s at %s\n", m.Name, m.Path) +// } +// +// On non-Linux platforms, stub implementations are provided that always +// return [LibraryNotFoundError]. +package libpath diff --git a/v3/internal/libpath/libpath_linux.go b/v3/internal/libpath/libpath_linux.go new file mode 100644 index 000000000..9b908d405 --- /dev/null +++ b/v3/internal/libpath/libpath_linux.go @@ -0,0 +1,551 @@ +//go:build linux + +package libpath + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" +) + +// Common library search paths on Linux systems +var defaultLibPaths = []string{ + // Standard paths + "/usr/lib", + "/usr/lib64", + "/lib", + "/lib64", + + // Debian/Ubuntu multiarch + "/usr/lib/x86_64-linux-gnu", + "/usr/lib/aarch64-linux-gnu", + "/usr/lib/i386-linux-gnu", + "/usr/lib/arm-linux-gnueabihf", + "/lib/x86_64-linux-gnu", + "/lib/aarch64-linux-gnu", + + // Fedora/RHEL/CentOS + "/usr/lib64/gtk-3.0", + "/usr/lib64/gtk-4.0", + "/usr/lib/gcc/x86_64-redhat-linux", + "/usr/lib/gcc/aarch64-redhat-linux", + + // Arch Linux + "/usr/lib/webkit2gtk-4.0", + "/usr/lib/webkit2gtk-4.1", + "/usr/lib/gtk-3.0", + "/usr/lib/gtk-4.0", + + // openSUSE + "/usr/lib64/gcc/x86_64-suse-linux", + + // Local installations + "/usr/local/lib", + "/usr/local/lib64", +} + +// searchResult holds the result from a parallel search goroutine. +type searchResult struct { + path string + source string // for debugging: "pkg-config", "ldconfig", "filesystem" +} + +// FindLibraryPath attempts to find the path to a library using multiple methods +// in parallel. It searches via pkg-config, ldconfig, and filesystem simultaneously, +// returning as soon as any method finds the library. +// +// The libName should be the pkg-config name (e.g., "gtk+-3.0", "webkit2gtk-4.1"). +// Returns the library directory path and any error encountered. +func FindLibraryPath(libName string) (string, error) { + return findLibraryPathCtx(context.Background(), libName) +} + +// FindLibraryPathSequential is the original sequential implementation. +// Use this if you need deterministic search order (pkg-config → ldconfig → filesystem). +func FindLibraryPathSequential(libName string) (string, error) { + // Try pkg-config first (most reliable when available) + if path, err := findWithPkgConfig(libName); err == nil { + return path, nil + } + + // Try ldconfig cache + if path, err := findWithLdconfig(libName); err == nil { + return path, nil + } + + // Fall back to searching common paths + return findInCommonPaths(libName) +} + +// FindLibraryFile finds the full path to a specific library file (e.g., "libgtk-3.so"). +func FindLibraryFile(fileName string) (string, error) { + // Try ldconfig first + if path, err := findFileWithLdconfig(fileName); err == nil { + return path, nil + } + + // Search all paths including dynamic ones + for _, dir := range GetAllLibPaths() { + // Check exact match + fullPath := filepath.Join(dir, fileName) + if _, err := os.Stat(fullPath); err == nil { + return fullPath, nil + } + + // Check with .so suffix variations + matches, err := filepath.Glob(filepath.Join(dir, fileName+"*")) + if err == nil && len(matches) > 0 { + return matches[0], nil + } + } + + return "", &LibraryNotFoundError{Name: fileName} +} + +// findWithPkgConfig uses pkg-config to find library paths. +func findWithPkgConfig(libName string) (string, error) { + return findWithPkgConfigCtx(context.Background(), libName) +} + +// findWithPkgConfigCtx uses pkg-config to find library paths with context support. +func findWithPkgConfigCtx(ctx context.Context, libName string) (string, error) { + // Check if already cancelled + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + } + + cmd := exec.CommandContext(ctx, "pkg-config", "--libs-only-L", libName) + output, err := cmd.Output() + if err != nil { + return "", err + } + + // Parse -L flags from output + parts := strings.Fields(string(output)) + for _, part := range parts { + if strings.HasPrefix(part, "-L") { + path := strings.TrimPrefix(part, "-L") + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + } + + // Check context before second command + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + } + + // If no -L flag, try --variable=libdir + cmd = exec.CommandContext(ctx, "pkg-config", "--variable=libdir", libName) + output, err = cmd.Output() + if err != nil { + return "", err + } + + path := strings.TrimSpace(string(output)) + if path != "" { + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + + return "", &LibraryNotFoundError{Name: libName} +} + +// findWithLdconfig searches the ldconfig cache for library paths. +func findWithLdconfig(libName string) (string, error) { + return findWithLdconfigCtx(context.Background(), libName) +} + +// findWithLdconfigCtx searches the ldconfig cache for library paths with context support. +func findWithLdconfigCtx(ctx context.Context, libName string) (string, error) { + // Check if already cancelled + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + } + + // Convert pkg-config name to library name pattern + // e.g., "gtk+-3.0" -> "libgtk-3", "webkit2gtk-4.1" -> "libwebkit2gtk-4.1" + searchName := pkgConfigToLibName(libName) + + cmd := exec.CommandContext(ctx, "ldconfig", "-p") + output, err := cmd.Output() + if err != nil { + return "", err + } + + for _, line := range strings.Split(string(output), "\n") { + if strings.Contains(line, searchName) { + // Line format: " libname.so.X (libc6,x86-64) => /path/to/lib" + parts := strings.Split(line, "=>") + if len(parts) == 2 { + libPath := strings.TrimSpace(parts[1]) + return filepath.Dir(libPath), nil + } + } + } + + return "", &LibraryNotFoundError{Name: libName} +} + +// findFileWithLdconfig finds a specific library file using ldconfig. +func findFileWithLdconfig(fileName string) (string, error) { + cmd := exec.Command("ldconfig", "-p") + output, err := cmd.Output() + if err != nil { + return "", err + } + + baseName := strings.TrimSuffix(fileName, ".so") + for _, line := range strings.Split(string(output), "\n") { + if strings.Contains(line, baseName) { + parts := strings.Split(line, "=>") + if len(parts) == 2 { + return strings.TrimSpace(parts[1]), nil + } + } + } + + return "", &LibraryNotFoundError{Name: fileName} +} + +// findInCommonPaths searches common library directories including +// dynamically discovered Flatpak, Snap, and Nix paths. +func findInCommonPaths(libName string) (string, error) { + return findInCommonPathsCtx(context.Background(), libName) +} + +// findInCommonPathsCtx searches common library directories with context support. +func findInCommonPathsCtx(ctx context.Context, libName string) (string, error) { + searchName := pkgConfigToLibName(libName) + + // Search all paths including dynamic ones + allPaths := GetAllLibPaths() + + for _, dir := range allPaths { + // Check if cancelled periodically + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + } + + if _, err := os.Stat(dir); err != nil { + continue + } + + // Look for the library file + pattern := filepath.Join(dir, searchName+"*.so*") + matches, err := filepath.Glob(pattern) + if err == nil && len(matches) > 0 { + return dir, nil + } + + // Also check pkgconfig subdirectory for .pc files + pcPath := filepath.Join(dir, "pkgconfig", libName+".pc") + if _, err := os.Stat(pcPath); err == nil { + return dir, nil + } + } + + return "", &LibraryNotFoundError{Name: libName} +} + +// pkgConfigToLibName converts a pkg-config package name to a library name pattern. +func pkgConfigToLibName(pkgName string) string { + // Common transformations + name := pkgName + + // Remove version suffix like "-3.0", "-4.1" + // but keep it for webkit2gtk-4.1 style names + if strings.HasPrefix(name, "gtk+-") { + // gtk+-3.0 -> libgtk-3 + name = "libgtk-" + strings.TrimPrefix(name, "gtk+-") + name = strings.Split(name, ".")[0] + } else if strings.HasPrefix(name, "webkit2gtk-") { + // webkit2gtk-4.1 -> libwebkit2gtk-4.1 + name = "lib" + name + } else if !strings.HasPrefix(name, "lib") { + name = "lib" + name + } + + return name +} + +// GetAllLibPaths returns all library paths from LD_LIBRARY_PATH, default paths, +// and dynamically discovered paths from Flatpak, Snap, and Nix. +// It does NOT include the current directory for security reasons. +func GetAllLibPaths() []string { + var paths []string + + // Add LD_LIBRARY_PATH entries first (highest priority) + if ldPath := os.Getenv("LD_LIBRARY_PATH"); ldPath != "" { + for _, p := range strings.Split(ldPath, ":") { + if p != "" { + paths = append(paths, p) + } + } + } + + // Add default system paths + paths = append(paths, defaultLibPaths...) + + // Add dynamically discovered paths from package managers + paths = append(paths, getFlatpakLibPaths()...) + paths = append(paths, getSnapLibPaths()...) + paths = append(paths, getNixLibPaths()...) + + return paths +} + +// FindOptions controls library search behavior. +type FindOptions struct { + // IncludeCurrentDir includes "." in the search path. + // WARNING: This is a security risk and should only be used for development. + IncludeCurrentDir bool + + // ExtraPaths are additional paths to search before the defaults. + ExtraPaths []string +} + +// FindLibraryPathWithOptions attempts to find the path to a library with custom options. +func FindLibraryPathWithOptions(libName string, opts FindOptions) (string, error) { + // Try pkg-config first (most reliable when available) + if path, err := findWithPkgConfig(libName); err == nil { + return path, nil + } + + // Try ldconfig cache + if path, err := findWithLdconfig(libName); err == nil { + return path, nil + } + + // Build search paths - include all dynamic paths too + allPaths := GetAllLibPaths() + searchPaths := make([]string, 0, len(opts.ExtraPaths)+len(allPaths)+1) + + if opts.IncludeCurrentDir { + if cwd, err := os.Getwd(); err == nil { + searchPaths = append(searchPaths, cwd) + } + } + + searchPaths = append(searchPaths, opts.ExtraPaths...) + searchPaths = append(searchPaths, allPaths...) + + // Search the paths + searchName := pkgConfigToLibName(libName) + for _, dir := range searchPaths { + if _, err := os.Stat(dir); err != nil { + continue + } + + pattern := filepath.Join(dir, searchName+"*.so*") + matches, err := filepath.Glob(pattern) + if err == nil && len(matches) > 0 { + return dir, nil + } + + pcPath := filepath.Join(dir, "pkgconfig", libName+".pc") + if _, err := os.Stat(pcPath); err == nil { + return dir, nil + } + } + + return "", &LibraryNotFoundError{Name: libName} +} + +// LibraryNotFoundError is returned when a library cannot be found. +type LibraryNotFoundError struct { + Name string +} + +func (e *LibraryNotFoundError) Error() string { + return "library not found: " + e.Name +} + +// LibraryMatch holds information about a found library. +type LibraryMatch struct { + // Name is the pkg-config name that was searched for. + Name string + // Path is the directory containing the library. + Path string +} + +// FindFirstLibrary searches for multiple libraries in parallel and returns +// the first one found. This is useful when you don't know the exact version +// of a library installed (e.g., gtk+-3.0 vs gtk+-4.0). +// +// The search order among candidates is non-deterministic - whichever is found +// first wins. If you need a specific preference order, list preferred libraries +// first and use FindFirstLibraryOrdered instead. +// +// Example: +// +// match, err := FindFirstLibrary("webkit2gtk-4.1", "webkit2gtk-4.0", "webkit2gtk-6.0") +// if err != nil { +// log.Fatal("No WebKit2GTK found") +// } +// fmt.Printf("Found %s at %s\n", match.Name, match.Path) +func FindFirstLibrary(libNames ...string) (*LibraryMatch, error) { + if len(libNames) == 0 { + return nil, &LibraryNotFoundError{Name: "no libraries specified"} + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + results := make(chan *LibraryMatch, len(libNames)) + var wg sync.WaitGroup + + for _, name := range libNames { + wg.Add(1) + go func(libName string) { + defer wg.Done() + if path, err := findLibraryPathCtx(ctx, libName); err == nil { + select { + case results <- &LibraryMatch{Name: libName, Path: path}: + case <-ctx.Done(): + } + } + }(name) + } + + // Close results when all goroutines complete + go func() { + wg.Wait() + close(results) + }() + + if result := <-results; result != nil { + return result, nil + } + + return nil, &LibraryNotFoundError{Name: strings.Join(libNames, ", ")} +} + +// FindFirstLibraryOrdered searches for libraries in order of preference, +// returning the first one found. Unlike FindFirstLibrary, this respects +// the order of candidates - earlier entries are preferred. +// +// This is useful when you want to prefer newer library versions: +// +// match, err := FindFirstLibraryOrdered("gtk+-4.0", "gtk+-3.0") +// // Will return gtk+-4.0 if available, otherwise gtk+-3.0 +func FindFirstLibraryOrdered(libNames ...string) (*LibraryMatch, error) { + if len(libNames) == 0 { + return nil, &LibraryNotFoundError{Name: "no libraries specified"} + } + + for _, name := range libNames { + if path, err := FindLibraryPath(name); err == nil { + return &LibraryMatch{Name: name, Path: path}, nil + } + } + + return nil, &LibraryNotFoundError{Name: strings.Join(libNames, ", ")} +} + +// FindAllLibraries searches for multiple libraries in parallel and returns +// all that are found. This is useful for discovering which library versions +// are available on the system. +// +// Example: +// +// matches := FindAllLibraries("gtk+-3.0", "gtk+-4.0", "webkit2gtk-4.0", "webkit2gtk-4.1") +// for _, m := range matches { +// fmt.Printf("Found %s at %s\n", m.Name, m.Path) +// } +func FindAllLibraries(libNames ...string) []LibraryMatch { + if len(libNames) == 0 { + return nil + } + + results := make(chan *LibraryMatch, len(libNames)) + var wg sync.WaitGroup + + for _, name := range libNames { + wg.Add(1) + go func(libName string) { + defer wg.Done() + if path, err := FindLibraryPath(libName); err == nil { + results <- &LibraryMatch{Name: libName, Path: path} + } + }(name) + } + + // Close results when all goroutines complete + go func() { + wg.Wait() + close(results) + }() + + var matches []LibraryMatch + for result := range results { + matches = append(matches, *result) + } + + return matches +} + +// findLibraryPathCtx is FindLibraryPath with context support. +func findLibraryPathCtx(ctx context.Context, libName string) (string, error) { + // Create a child context for this search + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + results := make(chan searchResult, 3) + var wg sync.WaitGroup + wg.Add(3) + + go func() { + defer wg.Done() + if path, err := findWithPkgConfigCtx(ctx, libName); err == nil { + select { + case results <- searchResult{path: path, source: "pkg-config"}: + case <-ctx.Done(): + } + } + }() + + go func() { + defer wg.Done() + if path, err := findWithLdconfigCtx(ctx, libName); err == nil { + select { + case results <- searchResult{path: path, source: "ldconfig"}: + case <-ctx.Done(): + } + } + }() + + go func() { + defer wg.Done() + if path, err := findInCommonPathsCtx(ctx, libName); err == nil { + select { + case results <- searchResult{path: path, source: "filesystem"}: + case <-ctx.Done(): + } + } + }() + + go func() { + wg.Wait() + close(results) + }() + + if result, ok := <-results; ok { + return result.path, nil + } + + return "", &LibraryNotFoundError{Name: libName} +} diff --git a/v3/internal/libpath/libpath_linux_test.go b/v3/internal/libpath/libpath_linux_test.go new file mode 100644 index 000000000..0f2e91bef --- /dev/null +++ b/v3/internal/libpath/libpath_linux_test.go @@ -0,0 +1,769 @@ +//go:build linux + +package libpath + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestPkgConfigToLibName(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"gtk+-3.0", "libgtk-3"}, + {"gtk+-4.0", "libgtk-4"}, + {"webkit2gtk-4.1", "libwebkit2gtk-4.1"}, + {"webkit2gtk-4.0", "libwebkit2gtk-4.0"}, + {"glib-2.0", "libglib-2.0"}, + {"libsoup-3.0", "libsoup-3.0"}, + {"cairo", "libcairo"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := pkgConfigToLibName(tt.input) + if result != tt.expected { + t.Errorf("pkgConfigToLibName(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestGetAllLibPaths(t *testing.T) { + paths := GetAllLibPaths() + + if len(paths) == 0 { + t.Error("GetAllLibPaths() returned empty slice") + } + + // Check that default paths are included + hasUsrLib := false + for _, p := range paths { + if p == "/usr/lib" || p == "/usr/lib64" { + hasUsrLib = true + break + } + } + if !hasUsrLib { + t.Error("GetAllLibPaths() should include /usr/lib or /usr/lib64") + } +} + +func TestGetAllLibPaths_WithLDPath(t *testing.T) { + // Save and restore LD_LIBRARY_PATH + original := os.Getenv("LD_LIBRARY_PATH") + defer os.Setenv("LD_LIBRARY_PATH", original) + + testPath := "/test/custom/lib:/another/path" + os.Setenv("LD_LIBRARY_PATH", testPath) + + paths := GetAllLibPaths() + + // First paths should be from LD_LIBRARY_PATH + if len(paths) < 2 { + t.Fatal("Expected at least 2 paths") + } + if paths[0] != "/test/custom/lib" { + t.Errorf("First path should be /test/custom/lib, got %s", paths[0]) + } + if paths[1] != "/another/path" { + t.Errorf("Second path should be /another/path, got %s", paths[1]) + } +} + +func TestLibraryNotFoundError(t *testing.T) { + err := &LibraryNotFoundError{Name: "testlib"} + expected := "library not found: testlib" + if err.Error() != expected { + t.Errorf("Error() = %q, want %q", err.Error(), expected) + } +} + +func TestFindLibraryPath_NotFound(t *testing.T) { + _, err := FindLibraryPath("nonexistent-library-xyz-123") + if err == nil { + t.Error("Expected error for nonexistent library") + } + + var notFoundErr *LibraryNotFoundError + if _, ok := err.(*LibraryNotFoundError); !ok { + t.Errorf("Expected LibraryNotFoundError, got %T", err) + } else { + notFoundErr = err.(*LibraryNotFoundError) + if notFoundErr.Name != "nonexistent-library-xyz-123" { + t.Errorf("Error name = %q, want %q", notFoundErr.Name, "nonexistent-library-xyz-123") + } + } +} + +func TestFindLibraryFile_NotFound(t *testing.T) { + _, err := FindLibraryFile("libnonexistent-xyz-123.so") + if err == nil { + t.Error("Expected error for nonexistent library file") + } +} + +// Integration tests - these depend on system state +// They're skipped if the required tools/libraries aren't available + +func TestFindLibraryPath_WithPkgConfig(t *testing.T) { + // Skip if pkg-config is not available + if _, err := exec.LookPath("pkg-config"); err != nil { + t.Skip("pkg-config not available") + } + + // Try to find a common library that's likely installed + commonLibs := []string{"glib-2.0", "zlib"} + + for _, lib := range commonLibs { + // Check if pkg-config knows about this library + cmd := exec.Command("pkg-config", "--exists", lib) + if cmd.Run() != nil { + continue + } + + t.Run(lib, func(t *testing.T) { + path, err := FindLibraryPath(lib) + if err != nil { + t.Errorf("FindLibraryPath(%q) failed: %v", lib, err) + return + } + + // Verify the path exists + if _, err := os.Stat(path); err != nil { + t.Errorf("Returned path %q does not exist", path) + } + }) + return // Only need to test one + } + + t.Skip("No common libraries found via pkg-config") +} + +func TestFindLibraryFile_Integration(t *testing.T) { + // Try to find libc which should exist on any Linux system + libcNames := []string{"libc.so.6", "libc.so"} + + for _, name := range libcNames { + path, err := FindLibraryFile(name) + if err == nil { + // Verify the path exists + if _, err := os.Stat(path); err != nil { + t.Errorf("Returned path %q does not exist", path) + } + return + } + } + + t.Skip("Could not find libc.so - unusual system configuration") +} + +func TestFindInCommonPaths(t *testing.T) { + // Create a temporary directory structure for testing + tmpDir := t.TempDir() + + // Create a fake library directory with a fake .so file + libDir := filepath.Join(tmpDir, "lib") + if err := os.MkdirAll(libDir, 0755); err != nil { + t.Fatal(err) + } + + // Create a fake library file + fakeLib := filepath.Join(libDir, "libfaketest.so.1") + if err := os.WriteFile(fakeLib, []byte{}, 0644); err != nil { + t.Fatal(err) + } + + // Temporarily add our test dir to defaultLibPaths + originalPaths := defaultLibPaths + defaultLibPaths = append([]string{libDir}, defaultLibPaths...) + defer func() { defaultLibPaths = originalPaths }() + + // Now test finding it + path, err := findInCommonPaths("faketest") + if err != nil { + t.Errorf("findInCommonPaths(\"faketest\") failed: %v", err) + return + } + + if path != libDir { + t.Errorf("findInCommonPaths(\"faketest\") = %q, want %q", path, libDir) + } +} + +func TestFindWithLdconfig(t *testing.T) { + // Skip if ldconfig is not available + if _, err := exec.LookPath("ldconfig"); err != nil { + t.Skip("ldconfig not available") + } + + // Check if we can run ldconfig -p + cmd := exec.Command("ldconfig", "-p") + output, err := cmd.Output() + if err != nil { + t.Skip("ldconfig -p failed") + } + + // Find any library from the output to test with + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "=>") && strings.Contains(line, "libc.so") { + // We found libc, try to find it + path, err := findWithLdconfig("glib-2.0") // Common library + if err == nil { + if _, statErr := os.Stat(path); statErr != nil { + t.Errorf("Returned path %q does not exist", path) + } + return + } + // If glib not found, that's okay - just means it's not installed + break + } + } +} + +func TestFindLibraryPathWithOptions_IncludeCurrentDir(t *testing.T) { + // Create a temporary directory and change to it + tmpDir := t.TempDir() + + // Create a fake library file in the temp dir + fakeLib := filepath.Join(tmpDir, "libcwdtest.so.1") + if err := os.WriteFile(fakeLib, []byte{}, 0644); err != nil { + t.Fatal(err) + } + + // Save current directory + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(origDir) + + // Change to temp directory + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + + // Without IncludeCurrentDir, should not find it + _, err = FindLibraryPathWithOptions("cwdtest", FindOptions{IncludeCurrentDir: false}) + if err == nil { + t.Error("Expected error without IncludeCurrentDir") + } + + // With IncludeCurrentDir, should find it + path, err := FindLibraryPathWithOptions("cwdtest", FindOptions{IncludeCurrentDir: true}) + if err != nil { + t.Errorf("FindLibraryPathWithOptions with IncludeCurrentDir failed: %v", err) + return + } + + if path != tmpDir { + t.Errorf("Expected path %q, got %q", tmpDir, path) + } +} + +func TestFindLibraryPathWithOptions_ExtraPaths(t *testing.T) { + // Create a temporary directory with a fake library + tmpDir := t.TempDir() + + fakeLib := filepath.Join(tmpDir, "libextratest.so.1") + if err := os.WriteFile(fakeLib, []byte{}, 0644); err != nil { + t.Fatal(err) + } + + // Should find it with ExtraPaths + path, err := FindLibraryPathWithOptions("extratest", FindOptions{ + ExtraPaths: []string{tmpDir}, + }) + if err != nil { + t.Errorf("FindLibraryPathWithOptions with ExtraPaths failed: %v", err) + return + } + + if path != tmpDir { + t.Errorf("Expected path %q, got %q", tmpDir, path) + } +} + +func TestDefaultLibPaths_ContainsDistros(t *testing.T) { + // Verify that paths for various distros are included + expectedPaths := map[string][]string{ + "Debian/Ubuntu": {"/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu"}, + "Fedora/RHEL": {"/usr/lib64/gtk-3.0", "/usr/lib64/gtk-4.0"}, + "Arch": {"/usr/lib/webkit2gtk-4.0", "/usr/lib/webkit2gtk-4.1"}, + "openSUSE": {"/usr/lib64/gcc/x86_64-suse-linux"}, + "Local": {"/usr/local/lib", "/usr/local/lib64"}, + } + + for distro, paths := range expectedPaths { + for _, path := range paths { + found := false + for _, defaultPath := range defaultLibPaths { + if defaultPath == path { + found = true + break + } + } + if !found { + t.Errorf("Missing %s path: %s", distro, path) + } + } + } +} + +func TestGetFlatpakLibPaths(t *testing.T) { + // This test just ensures the function doesn't panic + // Actual paths depend on system state + paths := getFlatpakLibPaths() + t.Logf("Found %d Flatpak lib paths", len(paths)) + for _, p := range paths { + t.Logf(" %s", p) + } +} + +func TestGetSnapLibPaths(t *testing.T) { + // This test just ensures the function doesn't panic + // Actual paths depend on system state + paths := getSnapLibPaths() + t.Logf("Found %d Snap lib paths", len(paths)) + for _, p := range paths { + t.Logf(" %s", p) + } +} + +func TestGetNixLibPaths(t *testing.T) { + // This test just ensures the function doesn't panic + paths := getNixLibPaths() + t.Logf("Found %d Nix lib paths", len(paths)) + for _, p := range paths { + t.Logf(" %s", p) + } +} + +func TestGetAllLibPaths_IncludesDynamicPaths(t *testing.T) { + paths := GetAllLibPaths() + + // Should have at least the default paths + if len(paths) < len(defaultLibPaths) { + t.Errorf("GetAllLibPaths returned fewer paths (%d) than defaultLibPaths (%d)", + len(paths), len(defaultLibPaths)) + } + + // Log all paths for debugging + t.Logf("Total paths: %d", len(paths)) +} + +func TestGetAllLibPaths_DoesNotIncludeCurrentDir(t *testing.T) { + paths := GetAllLibPaths() + + for _, p := range paths { + if p == "." { + t.Error("GetAllLibPaths should not include '.' for security reasons") + } + } +} + +func TestInvalidateCache(t *testing.T) { + // First call populates cache + paths1 := GetAllLibPaths() + + // Invalidate and call again + InvalidateCache() + paths2 := GetAllLibPaths() + + // Should get same results (assuming no system changes) + if len(paths1) != len(paths2) { + t.Logf("Path counts differ after invalidation: %d vs %d", len(paths1), len(paths2)) + // This is not necessarily an error, just informational + } + + // Verify cache is working by checking getFlatpakLibPaths is fast + // (would be slow if cache wasn't working) + for i := 0; i < 100; i++ { + _ = getFlatpakLibPaths() + } +} + +func TestFindLibraryPath_ParallelConsistency(t *testing.T) { + // Skip if pkg-config is not available + if _, err := exec.LookPath("pkg-config"); err != nil { + t.Skip("pkg-config not available") + } + + // Check if glib-2.0 is available + cmd := exec.Command("pkg-config", "--exists", "glib-2.0") + if cmd.Run() != nil { + t.Skip("glib-2.0 not installed") + } + + // Run parallel and sequential versions multiple times + // to ensure they return consistent results + for i := 0; i < 10; i++ { + parallelPath, parallelErr := FindLibraryPath("glib-2.0") + seqPath, seqErr := FindLibraryPathSequential("glib-2.0") + + if parallelErr != nil && seqErr == nil { + t.Errorf("Parallel failed but sequential succeeded: %v", parallelErr) + } + if parallelErr == nil && seqErr != nil { + t.Errorf("Sequential failed but parallel succeeded: %v", seqErr) + } + + // Both should find the library (path might differ if found by different methods) + if parallelErr != nil { + t.Errorf("Iteration %d: parallel search failed: %v", i, parallelErr) + } + if seqErr != nil { + t.Errorf("Iteration %d: sequential search failed: %v", i, seqErr) + } + + // Log paths for debugging + t.Logf("Iteration %d: parallel=%s, sequential=%s", i, parallelPath, seqPath) + } +} + +func TestFindLibraryPath_ParallelNotFound(t *testing.T) { + // Both parallel and sequential should return the same error for non-existent libs + _, parallelErr := FindLibraryPath("nonexistent-library-xyz-123") + _, seqErr := FindLibraryPathSequential("nonexistent-library-xyz-123") + + if parallelErr == nil { + t.Error("Parallel search should fail for nonexistent library") + } + if seqErr == nil { + t.Error("Sequential search should fail for nonexistent library") + } + + // Both should return LibraryNotFoundError + if _, ok := parallelErr.(*LibraryNotFoundError); !ok { + t.Errorf("Parallel: expected LibraryNotFoundError, got %T", parallelErr) + } + if _, ok := seqErr.(*LibraryNotFoundError); !ok { + t.Errorf("Sequential: expected LibraryNotFoundError, got %T", seqErr) + } +} + +// Benchmarks + +// BenchmarkFindLibraryPath benchmarks finding a library via the full search chain. +func BenchmarkFindLibraryPath(b *testing.B) { + // Test with glib-2.0 which is commonly installed + if _, err := exec.LookPath("pkg-config"); err != nil { + b.Skip("pkg-config not available") + } + cmd := exec.Command("pkg-config", "--exists", "glib-2.0") + if cmd.Run() != nil { + b.Skip("glib-2.0 not installed") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindLibraryPath("glib-2.0") + } +} + +// BenchmarkFindLibraryPath_NotFound benchmarks the worst case (library not found). +func BenchmarkFindLibraryPath_NotFound(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindLibraryPath("nonexistent-library-xyz-123") + } +} + +// BenchmarkFindLibraryFile benchmarks finding a specific library file. +func BenchmarkFindLibraryFile(b *testing.B) { + // libc.so.6 should exist on any Linux system + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindLibraryFile("libc.so.6") + } +} + +// BenchmarkGetAllLibPaths benchmarks collecting all library paths. +func BenchmarkGetAllLibPaths(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = GetAllLibPaths() + } +} + +// BenchmarkFindWithPkgConfig benchmarks pkg-config lookup directly. +func BenchmarkFindWithPkgConfig(b *testing.B) { + if _, err := exec.LookPath("pkg-config"); err != nil { + b.Skip("pkg-config not available") + } + cmd := exec.Command("pkg-config", "--exists", "glib-2.0") + if cmd.Run() != nil { + b.Skip("glib-2.0 not installed") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = findWithPkgConfig("glib-2.0") + } +} + +// BenchmarkFindWithLdconfig benchmarks ldconfig lookup directly. +func BenchmarkFindWithLdconfig(b *testing.B) { + if _, err := exec.LookPath("ldconfig"); err != nil { + b.Skip("ldconfig not available") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = findWithLdconfig("glib-2.0") + } +} + +// BenchmarkFindInCommonPaths benchmarks filesystem scanning. +func BenchmarkFindInCommonPaths(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = findInCommonPaths("glib-2.0") + } +} + +// BenchmarkGetFlatpakLibPaths benchmarks Flatpak path discovery. +func BenchmarkGetFlatpakLibPaths(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = getFlatpakLibPaths() + } +} + +// BenchmarkGetSnapLibPaths benchmarks Snap path discovery. +func BenchmarkGetSnapLibPaths(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = getSnapLibPaths() + } +} + +// BenchmarkGetNixLibPaths benchmarks Nix path discovery. +func BenchmarkGetNixLibPaths(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = getNixLibPaths() + } +} + +// BenchmarkPkgConfigToLibName benchmarks the name conversion function. +func BenchmarkPkgConfigToLibName(b *testing.B) { + names := []string{"gtk+-3.0", "webkit2gtk-4.1", "glib-2.0", "cairo", "libsoup-3.0"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, name := range names { + _ = pkgConfigToLibName(name) + } + } +} + +// BenchmarkFindLibraryPathSequential benchmarks the sequential search. +func BenchmarkFindLibraryPathSequential(b *testing.B) { + if _, err := exec.LookPath("pkg-config"); err != nil { + b.Skip("pkg-config not available") + } + cmd := exec.Command("pkg-config", "--exists", "glib-2.0") + if cmd.Run() != nil { + b.Skip("glib-2.0 not installed") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindLibraryPathSequential("glib-2.0") + } +} + +// BenchmarkFindLibraryPathSequential_NotFound benchmarks the sequential worst case. +func BenchmarkFindLibraryPathSequential_NotFound(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindLibraryPathSequential("nonexistent-library-xyz-123") + } +} + +// BenchmarkFindLibraryPathParallel explicitly tests parallel performance. +func BenchmarkFindLibraryPathParallel(b *testing.B) { + if _, err := exec.LookPath("pkg-config"); err != nil { + b.Skip("pkg-config not available") + } + cmd := exec.Command("pkg-config", "--exists", "glib-2.0") + if cmd.Run() != nil { + b.Skip("glib-2.0 not installed") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindLibraryPath("glib-2.0") + } +} + +// Tests for multi-library search functions + +func TestFindFirstLibrary(t *testing.T) { + if _, err := exec.LookPath("pkg-config"); err != nil { + t.Skip("pkg-config not available") + } + + // Test with a mix of existing and non-existing libraries + match, err := FindFirstLibrary("nonexistent-xyz", "glib-2.0", "also-nonexistent") + if err != nil { + t.Skipf("glib-2.0 not installed: %v", err) + } + + if match.Name != "glib-2.0" { + t.Errorf("Expected glib-2.0, got %s", match.Name) + } + if match.Path == "" { + t.Error("Expected non-empty path") + } +} + +func TestFindFirstLibrary_AllNotFound(t *testing.T) { + _, err := FindFirstLibrary("nonexistent-1", "nonexistent-2", "nonexistent-3") + if err == nil { + t.Error("Expected error for all non-existent libraries") + } +} + +func TestFindFirstLibrary_Empty(t *testing.T) { + _, err := FindFirstLibrary() + if err == nil { + t.Error("Expected error for empty library list") + } +} + +func TestFindFirstLibraryOrdered(t *testing.T) { + if _, err := exec.LookPath("pkg-config"); err != nil { + t.Skip("pkg-config not available") + } + + // glib-2.0 should be found, and since it's first, it should be returned + match, err := FindFirstLibraryOrdered("glib-2.0", "nonexistent-xyz") + if err != nil { + t.Skipf("glib-2.0 not installed: %v", err) + } + + if match.Name != "glib-2.0" { + t.Errorf("Expected glib-2.0, got %s", match.Name) + } +} + +func TestFindFirstLibraryOrdered_PreferFirst(t *testing.T) { + if _, err := exec.LookPath("pkg-config"); err != nil { + t.Skip("pkg-config not available") + } + + // Check what GTK versions are available + gtk4Available := exec.Command("pkg-config", "--exists", "gtk4").Run() == nil + gtk3Available := exec.Command("pkg-config", "--exists", "gtk+-3.0").Run() == nil + + if !gtk4Available && !gtk3Available { + t.Skip("Neither GTK3 nor GTK4 installed") + } + + // If both available, test that order is respected + if gtk4Available && gtk3Available { + match, err := FindFirstLibraryOrdered("gtk4", "gtk+-3.0") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if match.Name != "gtk4" { + t.Errorf("Expected gtk4 (first in order), got %s", match.Name) + } + + // Reverse order + match, err = FindFirstLibraryOrdered("gtk+-3.0", "gtk4") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if match.Name != "gtk+-3.0" { + t.Errorf("Expected gtk+-3.0 (first in order), got %s", match.Name) + } + } +} + +func TestFindAllLibraries(t *testing.T) { + if _, err := exec.LookPath("pkg-config"); err != nil { + t.Skip("pkg-config not available") + } + + matches := FindAllLibraries("glib-2.0", "nonexistent-xyz", "zlib") + + // Should find at least glib-2.0 on most systems + if len(matches) == 0 { + t.Skip("No common libraries found") + } + + t.Logf("Found %d libraries:", len(matches)) + for _, m := range matches { + t.Logf(" %s at %s", m.Name, m.Path) + } + + // Verify no duplicates and no nonexistent library + seen := make(map[string]bool) + for _, m := range matches { + if m.Name == "nonexistent-xyz" { + t.Error("Should not have found nonexistent library") + } + if seen[m.Name] { + t.Errorf("Duplicate match for %s", m.Name) + } + seen[m.Name] = true + } +} + +func TestFindAllLibraries_Empty(t *testing.T) { + matches := FindAllLibraries() + if len(matches) != 0 { + t.Error("Expected empty result for empty input") + } +} + +func TestFindAllLibraries_AllNotFound(t *testing.T) { + matches := FindAllLibraries("nonexistent-1", "nonexistent-2") + if len(matches) != 0 { + t.Errorf("Expected empty result, got %d matches", len(matches)) + } +} + +// Benchmarks for multi-library search + +func BenchmarkFindFirstLibrary(b *testing.B) { + if _, err := exec.LookPath("pkg-config"); err != nil { + b.Skip("pkg-config not available") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindFirstLibrary("nonexistent-1", "glib-2.0", "nonexistent-2") + } +} + +func BenchmarkFindFirstLibraryOrdered(b *testing.B) { + if _, err := exec.LookPath("pkg-config"); err != nil { + b.Skip("pkg-config not available") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = FindFirstLibraryOrdered("nonexistent-1", "glib-2.0", "nonexistent-2") + } +} + +func BenchmarkFindAllLibraries(b *testing.B) { + if _, err := exec.LookPath("pkg-config"); err != nil { + b.Skip("pkg-config not available") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = FindAllLibraries("glib-2.0", "zlib", "nonexistent-xyz") + } +} diff --git a/v3/internal/libpath/libpath_other.go b/v3/internal/libpath/libpath_other.go new file mode 100644 index 000000000..d1c7bc77d --- /dev/null +++ b/v3/internal/libpath/libpath_other.go @@ -0,0 +1,73 @@ +//go:build !linux + +package libpath + +// FindLibraryPath is a stub for non-Linux platforms. +func FindLibraryPath(libName string) (string, error) { + return "", &LibraryNotFoundError{Name: libName} +} + +// FindLibraryFile is a stub for non-Linux platforms. +func FindLibraryFile(fileName string) (string, error) { + return "", &LibraryNotFoundError{Name: fileName} +} + +// GetAllLibPaths returns an empty slice on non-Linux platforms. +func GetAllLibPaths() []string { + return nil +} + +// InvalidateCache is a no-op on non-Linux platforms. +func InvalidateCache() {} + +// FindOptions controls library search behavior. +type FindOptions struct { + IncludeCurrentDir bool + ExtraPaths []string +} + +// FindLibraryPathWithOptions is a stub for non-Linux platforms. +func FindLibraryPathWithOptions(libName string, opts FindOptions) (string, error) { + return "", &LibraryNotFoundError{Name: libName} +} + +// LibraryNotFoundError is returned when a library cannot be found. +type LibraryNotFoundError struct { + Name string +} + +func (e *LibraryNotFoundError) Error() string { + return "library not found: " + e.Name +} + +// LibraryMatch holds information about a found library. +type LibraryMatch struct { + Name string + Path string +} + +// FindFirstLibrary is a stub for non-Linux platforms. +func FindFirstLibrary(libNames ...string) (*LibraryMatch, error) { + if len(libNames) == 0 { + return nil, &LibraryNotFoundError{Name: "no libraries specified"} + } + return nil, &LibraryNotFoundError{Name: libNames[0]} +} + +// FindFirstLibraryOrdered is a stub for non-Linux platforms. +func FindFirstLibraryOrdered(libNames ...string) (*LibraryMatch, error) { + if len(libNames) == 0 { + return nil, &LibraryNotFoundError{Name: "no libraries specified"} + } + return nil, &LibraryNotFoundError{Name: libNames[0]} +} + +// FindAllLibraries is a stub for non-Linux platforms. +func FindAllLibraries(libNames ...string) []LibraryMatch { + return nil +} + +// FindLibraryPathSequential is a stub for non-Linux platforms. +func FindLibraryPathSequential(libName string) (string, error) { + return "", &LibraryNotFoundError{Name: libName} +} diff --git a/v3/internal/libpath/nix_linux.go b/v3/internal/libpath/nix_linux.go new file mode 100644 index 000000000..74d8487a9 --- /dev/null +++ b/v3/internal/libpath/nix_linux.go @@ -0,0 +1,28 @@ +//go:build linux + +package libpath + +import "os" + +// getNixLibPaths returns cached library paths for Nix/NixOS installations. +func getNixLibPaths() []string { + return cache.getNix() +} + +// discoverNixLibPaths scans for Nix library paths. +func discoverNixLibPaths() []string { + var paths []string + + nixProfileLib := os.ExpandEnv("$HOME/.nix-profile/lib") + if _, err := os.Stat(nixProfileLib); err == nil { + paths = append(paths, nixProfileLib) + } + + // System Nix store - packages expose libs through profiles + nixStoreLib := "/run/current-system/sw/lib" + if _, err := os.Stat(nixStoreLib); err == nil { + paths = append(paths, nixStoreLib) + } + + return paths +} diff --git a/v3/internal/libpath/snap_linux.go b/v3/internal/libpath/snap_linux.go new file mode 100644 index 000000000..99def76ac --- /dev/null +++ b/v3/internal/libpath/snap_linux.go @@ -0,0 +1,42 @@ +//go:build linux + +package libpath + +import ( + "os" + "path/filepath" +) + +// getSnapLibPaths returns cached library paths from installed Snap packages. +func getSnapLibPaths() []string { + return cache.getSnap() +} + +// discoverSnapLibPaths scans for Snap package library directories. +// Scans /snap/*/current/usr/lib* directories. +func discoverSnapLibPaths() []string { + var paths []string + + snapDir := "/snap" + if _, err := os.Stat(snapDir); err != nil { + return paths + } + + // Find all snap packages with lib directories + patterns := []string{ + filepath.Join(snapDir, "*", "current", "usr", "lib"), + filepath.Join(snapDir, "*", "current", "usr", "lib64"), + filepath.Join(snapDir, "*", "current", "usr", "lib", "*-linux-gnu"), + filepath.Join(snapDir, "*", "current", "lib"), + filepath.Join(snapDir, "*", "current", "lib", "*-linux-gnu"), + } + + for _, pattern := range patterns { + matches, err := filepath.Glob(pattern) + if err == nil { + paths = append(paths, matches...) + } + } + + return paths +}