Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
38 changes: 32 additions & 6 deletions manager/apt/apt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -102,20 +128,20 @@ 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.
// It verifies both that apt exists and that it's the Debian apt package manager
// (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
}
Expand All @@ -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
}
Expand All @@ -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.
Expand Down
82 changes: 72 additions & 10 deletions syspkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading