mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
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:
parent
722717a11e
commit
f1a4ffe72d
9 changed files with 1700 additions and 0 deletions
|
|
@ -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 -->
|
||||
|
|
|
|||
79
v3/internal/libpath/cache_linux.go
Normal file
79
v3/internal/libpath/cache_linux.go
Normal 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()
|
||||
}
|
||||
53
v3/internal/libpath/flatpak_linux.go
Normal file
53
v3/internal/libpath/flatpak_linux.go
Normal 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
|
||||
}
|
||||
104
v3/internal/libpath/libpath.go
Normal file
104
v3/internal/libpath/libpath.go
Normal 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
|
||||
551
v3/internal/libpath/libpath_linux.go
Normal file
551
v3/internal/libpath/libpath_linux.go
Normal 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}
|
||||
}
|
||||
769
v3/internal/libpath/libpath_linux_test.go
Normal file
769
v3/internal/libpath/libpath_linux_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
73
v3/internal/libpath/libpath_other.go
Normal file
73
v3/internal/libpath/libpath_other.go
Normal 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}
|
||||
}
|
||||
28
v3/internal/libpath/nix_linux.go
Normal file
28
v3/internal/libpath/nix_linux.go
Normal 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
|
||||
}
|
||||
42
v3/internal/libpath/snap_linux.go
Normal file
42
v3/internal/libpath/snap_linux.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue