feat(linux): add libpath package for finding native library paths (#4847)

* feat(linux): add libpath package for finding native library paths

Add a new internal/libpath package that locates shared libraries (.so files)
on Linux systems. Supports multiple distributions and package managers.

Features:
- Multi-tier search: pkg-config -> ldconfig -> filesystem scanning
- Parallel search using goroutines for faster lookups
- Cached dynamic path discovery for Flatpak, Snap, and Nix
- Support for Debian/Ubuntu, Fedora/RHEL, Arch, openSUSE, NixOS
- Context-aware cancellation for graceful shutdown

Performance:
- Library found: ~1.4ms (parallel search)
- Library not found: ~46ms (was 84ms sequential)
- Cached path discovery: 14ns (was 15ms uncached)

* feat(libpath): add multi-library parallel search functions

Add functions to search for multiple library candidates in parallel:

- FindFirstLibrary: Search multiple libs in parallel, return first found
- FindFirstLibraryOrdered: Search in order of preference (for version priority)
- FindAllLibraries: Find all available libraries from a list

Useful when the exact library version is unknown, e.g.:
  match, _ := FindFirstLibrary("webkit2gtk-4.1", "webkit2gtk-4.0", "webkit2gtk-6.0")

Also adds findLibraryPathCtx for context-aware searching used by the
multi-library functions.

* refactor(libpath): split into separate files and fix race condition

Split libpath_linux.go into smaller, focused files:
- cache_linux.go: Path cache with thread-safe init/invalidate
- flatpak_linux.go: Flatpak runtime path discovery
- snap_linux.go: Snap package path discovery
- nix_linux.go: Nix/NixOS path discovery
- libpath_linux.go: Core search functions

Fixes:
- Fix data race between init() and invalidate() by holding mutex
  during cache writes inside sync.Once.Do (CodeRabbit review)
- Fix FindLibraryPathWithOptions not searching dynamic paths
  (Flatpak/Snap/Nix) - now uses GetAllLibPaths() (CodeRabbit review)
This commit is contained in:
Lea Anthony 2026-01-04 11:59:22 +11:00 committed by GitHub
commit f1a4ffe72d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1700 additions and 0 deletions

View file

@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file
## Added
<!-- New features, capabilities, or enhancements -->
- Add `internal/libpath` package for finding native library paths on Linux with parallel search, caching, and support for Flatpak/Snap/Nix
## Changed
<!-- Changes in existing functionality -->

View file

@ -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()
}

View file

@ -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/<name>/<arch>/<version>/<hash>/files/lib
matches, err := filepath.Glob(filepath.Join(runtimeDir, "*", "*", "*", "*", "files", "lib"))
if err == nil {
paths = append(paths, matches...)
}
}
return paths
}

View file

@ -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

View file

@ -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}
}

View file

@ -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")
}
}

View file

@ -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}
}

View file

@ -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
}

View file

@ -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
}