limbo/bindings/go/embedded.go

133 lines
3.9 KiB
Go

// Go bindings for the Limbo database.
//
// This file implements library embedding and extraction at runtime, a pattern
// also used in several other Go projects that need to distribute native binaries:
//
// - github.com/kluctl/go-embed-python: Embeds a full Python distribution in Go
// binaries, extracting to temporary directories at runtime. The approach used here
// was directly inspired by its embed_util implementation.
//
// - github.com/kluctl/go-jinja2: Uses the same pattern to embed Jinja2 and related
// Python libraries, allowing Go applications to use Jinja2 templates without
// external dependencies.
//
// This approach has several advantages:
// - Allows distribution of a single, self-contained binary
// - Eliminates the need for users to set LD_LIBRARY_PATH or other environment variables
// - Works cross-platform with the same codebase
// - Preserves backward compatibility with existing methods
// - Extracts libraries only once per execution via sync.Once
//
// The embedded library is extracted to a user-specific temporary directory and
// loaded dynamically. If extraction fails, the code falls back to the traditional
// method of searching system paths.
package limbo
import (
"embed"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sync"
)
//go:embed libs/*
var embeddedLibs embed.FS
var (
extractOnce sync.Once
extractedPath string
extractErr error
)
// extractEmbeddedLibrary extracts the library for the current platform
// to a temporary directory and returns the path to the extracted library
func extractEmbeddedLibrary() (string, error) {
extractOnce.Do(func() {
// Determine platform-specific details
var libName string
var platformDir string
switch runtime.GOOS {
case "darwin":
libName = "lib_limbo_go.dylib"
case "linux":
libName = "lib_limbo_go.so"
case "windows":
libName = "lib_limbo_go.dll"
default:
extractErr = fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
return
}
// Determine architecture suffix
var archSuffix string
switch runtime.GOARCH {
case "amd64":
archSuffix = "amd64"
case "arm64":
archSuffix = "arm64"
case "386":
archSuffix = "386"
default:
extractErr = fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
return
}
// Create platform directory string
platformDir = fmt.Sprintf("%s_%s", runtime.GOOS, archSuffix)
// Create a unique temporary directory for the current user
tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("limbo-go-%d", os.Getuid()))
if err := os.MkdirAll(tempDir, 0755); err != nil {
extractErr = fmt.Errorf("failed to create temp directory: %w", err)
return
}
// Path to the library within the embedded filesystem
libPath := filepath.Join("libs", platformDir, libName)
// Where the library will be extracted
extractedPath = filepath.Join(tempDir, libName)
// Check if library already exists and is valid
if stat, err := os.Stat(extractedPath); err == nil && stat.Size() > 0 {
// Library already exists, nothing to do
return
}
// Open the embedded library
embeddedLib, err := embeddedLibs.Open(libPath)
if err != nil {
extractErr = fmt.Errorf("failed to open embedded library %s: %w", libPath, err)
return
}
defer embeddedLib.Close()
// Create the output file
outFile, err := os.Create(extractedPath)
if err != nil {
extractErr = fmt.Errorf("failed to create output file: %w", err)
return
}
defer outFile.Close()
// Copy the library to the temporary directory
if _, err := io.Copy(outFile, embeddedLib); err != nil {
extractErr = fmt.Errorf("failed to extract library: %w", err)
return
}
// On Unix systems, make the library executable
if runtime.GOOS != "windows" {
if err := os.Chmod(extractedPath, 0755); err != nil {
extractErr = fmt.Errorf("failed to make library executable: %w", err)
return
}
}
})
return extractedPath, extractErr
}