diff --git a/interface.go b/interface.go index 52010b5..6a64eb7 100644 --- a/interface.go +++ b/interface.go @@ -71,6 +71,15 @@ type PackageManager interface { AutoRemove(opts *manager.Options) ([]manager.PackageInfo, error) } +// ManagerCreationOptions specifies options for creating a package manager instance. +type ManagerCreationOptions struct { + // BinaryPath specifies a custom binary path or name to use for the package manager. + // This can be either just a binary name (e.g., "apt-fast") which will be searched in PATH, + // or a full path (e.g., "/usr/local/bin/apt-fast"). + // If empty, the default binary for the package manager will be used. + BinaryPath string +} + // SysPkg is the interface that defines the methods for interacting with the SysPkg library. type SysPkg interface { // FindPackageManagers returns a map of available package managers based on the specified IncludeOptions. @@ -89,8 +98,25 @@ type SysPkg interface { // If the name is empty, the first available package manager will be returned. // If no suitable package manager is found, an error is returned. // Note: only package managers that are specified in the IncludeOptions when creating the SysPkg instance (with New() method) will be returned. If you want to use package managers that are not specified in the IncludeOptions, you should use the FindPackageManagers() method to get a list of all available package managers, or use RefreshPackageManagers() with the IncludeOptions parameter to refresh the package manager list. + // For custom binary paths, use GetPackageManagerWithOptions. GetPackageManager(name string) (PackageManager, error) + // GetPackageManagerWithOptions returns a PackageManager instance with custom configuration options. + // This allows specifying custom binary paths (e.g., "apt-fast" instead of "apt") and other creation-time options. + // The returned manager instance will use the specified options for all operations. + // + // Example usage: + // // Use apt-fast instead of apt + // apt, _ := syspkg.GetPackageManagerWithOptions("apt", &ManagerCreationOptions{ + // BinaryPath: "apt-fast", + // }) + // + // // Use custom yum path + // yum, _ := syspkg.GetPackageManagerWithOptions("yum", &ManagerCreationOptions{ + // BinaryPath: "/opt/custom/yum", + // }) + GetPackageManagerWithOptions(name string, opts *ManagerCreationOptions) (PackageManager, error) + // Install(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) // Delete(pkgs []string, opts *manager.Options) ([]manager.PackageInfo, error) // Find(keywords []string, opts *manager.Options) ([]manager.PackageInfo, error) diff --git a/manager/apt/apt.go b/manager/apt/apt.go index 18a7071..62fcd9e 100644 --- a/manager/apt/apt.go +++ b/manager/apt/apt.go @@ -64,6 +64,11 @@ type PackageManager struct { // runnerOnce protects lazy initialization for zero-value struct usage (e.g., &PackageManager{}) // This enables defensive programming and backward compatibility with existing test patterns runnerOnce sync.Once + // binaryName is the name of the binary to use (e.g., "apt", "apt-fast") + // Defaults to "apt" if not specified + binaryName string + // binaryOnce protects lazy initialization of binaryName + binaryOnce sync.Once } // NewPackageManager creates a new APT package manager with default command runner @@ -77,10 +82,31 @@ func NewPackageManager() *PackageManager { // This is primarily used for testing with mocked commands func NewPackageManagerWithCustomRunner(runner manager.CommandRunner) *PackageManager { return &PackageManager{ - runner: runner, + runner: runner, + binaryName: pm, } } +// NewPackageManagerWithBinary creates a new APT package manager with a custom binary name +// This allows using apt-compatible binaries like apt-fast as a drop-in replacement +func NewPackageManagerWithBinary(binaryName string) *PackageManager { + return &PackageManager{ + runner: manager.NewDefaultCommandRunner(), + binaryName: binaryName, + } +} + +// getBinaryName returns the binary name, defaulting to "apt" if not set. +// Uses sync.Once for thread-safe lazy initialization to support zero-value struct usage. +func (a *PackageManager) getBinaryName() string { + a.binaryOnce.Do(func() { + if a.binaryName == "" { + a.binaryName = pm + } + }) + return a.binaryName +} + // getRunner returns the command runner, creating a default one if not set. // Uses sync.Once for thread-safe lazy initialization to support zero-value struct usage: // - Production: NewPackageManager() pre-initializes runner @@ -102,12 +128,12 @@ func (a *PackageManager) getRunner() manager.CommandRunner { func (a *PackageManager) executeCommand(ctx context.Context, args []string, opts *manager.Options) ([]byte, error) { if opts != nil && opts.Interactive { // Interactive mode uses RunInteractive for stdin/stdout/stderr handling - err := a.getRunner().RunInteractive(ctx, pm, args, aptNonInteractiveEnv...) + err := a.getRunner().RunInteractive(ctx, a.getBinaryName(), args, aptNonInteractiveEnv...) return nil, err } // Use RunContext for non-interactive execution (automatically includes LC_ALL=C) - return a.getRunner().RunContext(ctx, pm, args, aptNonInteractiveEnv...) + return a.getRunner().RunContext(ctx, a.getBinaryName(), args, aptNonInteractiveEnv...) } // IsAvailable checks if the apt package manager is available on the system. @@ -115,7 +141,7 @@ func (a *PackageManager) executeCommand(ctx context.Context, args []string, opts // (not the Java Annotation Processing Tool with the same name on some systems). func (a *PackageManager) IsAvailable() bool { // First check if apt command exists - _, err := exec.LookPath(pm) + _, err := exec.LookPath(a.getBinaryName()) if err != nil { return false } @@ -128,7 +154,7 @@ func (a *PackageManager) IsAvailable() bool { // Test if this is actually functional Debian apt by trying a safe command // This approach: if apt+dpkg work together, support them regardless of platform - output, err := a.getRunner().Run("apt", "--version") + output, err := a.getRunner().Run(a.getBinaryName(), "--version") if err != nil { return false } @@ -144,7 +170,7 @@ func (a *PackageManager) IsAvailable() bool { // GetPackageManager returns the name of the apt package manager. func (a *PackageManager) GetPackageManager() string { - return pm + return a.getBinaryName() } // Install installs the provided packages using the apt package manager. diff --git a/syspkg.go b/syspkg.go index ccbc817..1c544a5 100644 --- a/syspkg.go +++ b/syspkg.go @@ -102,27 +102,89 @@ func (s *sysPkgImpl) FindPackageManagers(include IncludeOptions) (map[string]Pac // GetPackageManager returns a PackageManager instance by its name (e.g., "apt", "snap", "flatpak", etc.). // if name is empty, return the first available +// For custom binary paths, use GetPackageManagerWithOptions. func (s *sysPkgImpl) GetPackageManager(name string) (PackageManager, error) { + return s.GetPackageManagerWithOptions(name, nil) +} + +// GetPackageManagerWithOptions returns a PackageManager instance with custom configuration options. +// This method provides a flexible way to create package manager instances with custom binaries. +// +// Parameters: +// - name: The package manager name (e.g., "apt", "yum", "snap", "flatpak") +// - opts: Optional configuration. If nil, default configuration is used. +// +// When opts.BinaryPath is specified: +// - A new manager instance is created with the custom binary +// - The custom binary can be a name (searched in PATH) or full path +// - This is useful for binary variants like "apt-fast" or custom installations +// +// When opts is nil or opts.BinaryPath is empty: +// - Returns the manager from the pre-registered list (created via New()) +// - This is the standard case for default package managers +// +// Example usage: +// +// // Get default apt +// apt, _ := syspkg.GetPackageManager("apt") +// +// // Get apt with apt-fast binary +// aptFast, _ := syspkg.GetPackageManagerWithOptions("apt", &ManagerCreationOptions{ +// BinaryPath: "apt-fast", +// }) +// +// // Get yum with custom path +// customYum, _ := syspkg.GetPackageManagerWithOptions("yum", &ManagerCreationOptions{ +// BinaryPath: "/opt/custom/yum", +// }) +func (s *sysPkgImpl) GetPackageManagerWithOptions(name string, opts *ManagerCreationOptions) (PackageManager, error) { // if there are no package managers, return before accessing non existing properties if len(s.pms) == 0 { return nil, errors.New("no supported package manager detected") } - if name == "" { - // get first pm available, lexicographically sorted - keys := make([]string, 0, len(s.pms)) - for k := range s.pms { - keys = append(keys, k) + // Extract binary path from options + var binaryPath string + if opts != nil && opts.BinaryPath != "" { + binaryPath = opts.BinaryPath + } + + // If no custom binary path, use standard lookup + if binaryPath == "" { + if name == "" { + // get first pm available, lexicographically sorted + keys := make([]string, 0, len(s.pms)) + for k := range s.pms { + keys = append(keys, k) + } + sort.Strings(keys) + return s.pms[keys[0]], nil + } + + pm, found := s.pms[name] + if !found { + return nil, errors.New("no such package manager") } - sort.Strings(keys) - return s.pms[keys[0]], nil + return pm, nil } - pm, found := s.pms[name] - if !found { + // Custom binary path specified - create new instance + switch name { + case "apt": + return apt.NewPackageManagerWithBinary(binaryPath), nil + case "yum": + // YUM doesn't have NewPackageManagerWithBinary yet, but structure is ready + // For now, return error - will be implemented when YUM supports it + return nil, errors.New("custom binary path not yet supported for yum") + case "snap": + // Snap doesn't have NewPackageManagerWithBinary yet + return nil, errors.New("custom binary path not yet supported for snap") + case "flatpak": + // Flatpak doesn't have NewPackageManagerWithBinary yet + return nil, errors.New("custom binary path not yet supported for flatpak") + default: return nil, errors.New("no such package manager") } - return pm, nil } // RefreshPackageManagers refreshes the internal list of available package managers, and returns the new list.