feat: pacman/yay installers

This commit is contained in:
2025-12-03 21:01:37 +02:00
parent f5c1101e8a
commit 7de60d48fd
8 changed files with 293 additions and 2 deletions

View File

@@ -229,6 +229,11 @@ For a full list with all the supported options, see [the docs](./docs/installer-
- Installs packages using apt/apk install.
- Use `type: apt` for `apt install`, and `type: apk` for `apk add`.
- **`pacman`/`yay`**
- Installs packages using pacman or yay (Arch Linux).
- Use `type: pacman` for official repository packages, and `type: yay` for AUR packages.
- **`pipx`**
- Installs packages using pipx.

View File

@@ -65,6 +65,8 @@ const (
InstallerTypeYarn InstallerType = "yarn" // InstallerTypeYarn represents a yarn package installer.
InstallerTypePipx InstallerType = "pipx" // InstallerTypePipx represents a pipx package installer.
InstallerTypeManifest InstallerType = "manifest" // InstallerTypeManifest represents a manifest file installer.
InstallerTypePacman InstallerType = "pacman" // InstallerTypePacman represents a pacman package installer.
InstallerTypeYay InstallerType = "yay" // InstallerTypeYay represents a yay (AUR helper) package installer.
)
// Environ returns the combined environment variables for the installer as a slice of strings.

View File

@@ -212,6 +212,15 @@ These fields are shared by all installer types. Some fields may vary in behavior
- **Description**: Installs packages using apt install or apt add.
- Use `type: apt` for `apt install`, and `type: apk` for `apk add`.
- **`pacman`/`yay`**
- **Description**: Installs packages using pacman or yay (Arch Linux).
- Use `type: pacman` for official Arch repository packages.
- Use `type: yay` for AUR (Arch User Repository) packages.
- Both use `--noconfirm` for non-interactive installation.
- **Options**:
- `opts.needed`: Skip reinstalling up-to-date packages (`--needed` flag).
- **`pipx`**
- **Description**: Installs packages using pipx.
@@ -380,6 +389,23 @@ install:
only: ['linux']
```
### pacman/yay
```yaml
install:
# Install from official Arch repositories
- name: neovim
type: pacman
bin_name: nvim
opts:
needed: true # Skip if already up-to-date
# Install from AUR using yay
- name: visual-studio-code-bin
type: yay
bin_name: code
```
### docker
```yaml

View File

@@ -1,6 +1,4 @@
#!/usr/bin/env sh
# Portable installer for sofmani (no Bashisms)
# Env vars you can override: INSTALL_DIR, REPO
set -eu

View File

@@ -49,6 +49,8 @@ func GetInstaller(config *appconfig.AppConfig, data *appconfig.InstallerData) (I
return NewNpmInstaller(config, data), nil
case appconfig.InstallerTypeApt, appconfig.InstallerTypeApk:
return NewAptInstaller(config, data), nil
case appconfig.InstallerTypePacman, appconfig.InstallerTypeYay:
return NewPacmanInstaller(config, data), nil
case appconfig.InstallerTypePipx:
return NewPipxInstaller(config, data), nil
case appconfig.InstallerTypeGitHubRelease:

View File

@@ -127,5 +127,9 @@ func FillDefaults(data *appconfig.InstallerData) {
data.Platforms = &platform.Platforms{
Only: &[]platform.Platform{platform.PlatformLinux},
}
case appconfig.InstallerTypePacman, appconfig.InstallerTypeYay:
data.Platforms = &platform.Platforms{
Only: &[]platform.Platform{platform.PlatformLinux},
}
}
}

View File

@@ -0,0 +1,130 @@
package installer
import (
"github.com/chenasraf/sofmani/appconfig"
)
// PacmanInstaller is an installer for pacman and yay packages.
type PacmanInstaller struct {
InstallerBase
// Config is the application configuration.
Config *appconfig.AppConfig
// Info is the installer data.
Info *appconfig.InstallerData
// PackageManager is the package manager to use (pacman or yay).
PackageManager PacmanPackageManager
}
// PacmanOpts represents options for the PacmanInstaller.
type PacmanOpts struct {
// Needed skips reinstalling up-to-date packages (--needed flag).
Needed *bool
}
// PacmanPackageManager represents an Arch Linux package manager type.
type PacmanPackageManager string
// Constants for supported Arch Linux package managers.
const (
PackageManagerPacman PacmanPackageManager = "pacman" // PackageManagerPacman represents the pacman package manager.
PackageManagerYay PacmanPackageManager = "yay" // PackageManagerYay represents the yay AUR helper.
)
// Validate validates the installer configuration.
func (i *PacmanInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
return errors
}
// Install implements IInstaller.
func (i *PacmanInstaller) Install() error {
name := *i.Info.Name
args := []string{"-S", "--noconfirm"}
if i.GetOpts().Needed != nil && *i.GetOpts().Needed {
args = append(args, "--needed")
}
args = append(args, name)
return i.RunCmdPassThrough(string(i.PackageManager), args...)
}
// Update implements IInstaller.
func (i *PacmanInstaller) Update() error {
name := *i.Info.Name
args := []string{"-S", "--noconfirm"}
if i.GetOpts().Needed != nil && *i.GetOpts().Needed {
args = append(args, "--needed")
}
args = append(args, name)
return i.RunCmdPassThrough(string(i.PackageManager), args...)
}
// CheckNeedsUpdate implements IInstaller.
func (i *PacmanInstaller) CheckNeedsUpdate() (bool, error) {
if i.HasCustomUpdateCheck() {
return i.RunCustomUpdateCheck()
}
// -Qu lists packages that have updates available
// If the package has an update, it will be in the output
output, err := i.RunCmdGetOutput(string(i.PackageManager), "-Qu", *i.Info.Name)
if err != nil {
// No output or error means no updates needed
return false, nil
}
// If we got output, there are updates available
return len(output) > 0, nil
}
// CheckIsInstalled implements IInstaller.
func (i *PacmanInstaller) CheckIsInstalled() (bool, error) {
if i.HasCustomInstallCheck() {
return i.RunCustomInstallCheck()
}
// Use pacman -Q to check if package is installed (works for all packages including fonts/libraries)
return i.RunCmdGetSuccess(string(i.PackageManager), "-Q", *i.Info.Name)
}
// GetData implements IInstaller.
func (i *PacmanInstaller) GetData() *appconfig.InstallerData {
return i.Info
}
// GetOpts returns the parsed options for the PacmanInstaller.
func (i *PacmanInstaller) GetOpts() *PacmanOpts {
opts := &PacmanOpts{}
info := i.Info
if info.Opts != nil {
if needed, ok := (*info.Opts)["needed"].(bool); ok {
opts.Needed = &needed
}
}
return opts
}
// GetBinName returns the binary name for the installer.
// It uses the BinName from the installer data if provided, otherwise it uses the installer name.
func (i *PacmanInstaller) GetBinName() string {
info := i.GetData()
if info.BinName != nil && len(*info.BinName) > 0 {
return *info.BinName
}
return *info.Name
}
// NewPacmanInstaller creates a new PacmanInstaller.
func NewPacmanInstaller(cfg *appconfig.AppConfig, installer *appconfig.InstallerData) *PacmanInstaller {
var packageManager PacmanPackageManager
switch installer.Type {
case appconfig.InstallerTypePacman:
packageManager = PackageManagerPacman
case appconfig.InstallerTypeYay:
packageManager = PackageManagerYay
}
i := &PacmanInstaller{
InstallerBase: InstallerBase{Data: installer},
Config: cfg,
Info: installer,
PackageManager: packageManager,
}
return i
}

View File

@@ -0,0 +1,124 @@
package installer
import (
"testing"
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
)
func newTestPacmanInstaller(data *appconfig.InstallerData) *PacmanInstaller {
return &PacmanInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Config: nil,
PackageManager: PackageManagerPacman,
Info: data,
}
}
func newTestYayInstaller(data *appconfig.InstallerData) *PacmanInstaller {
return &PacmanInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Config: nil,
PackageManager: PackageManagerYay,
Info: data,
}
}
func TestPacmanValidation(t *testing.T) {
logger.InitLogger(false)
// Valid pacman installer
validPacmanData := &appconfig.InstallerData{
Name: strPtr("vim"),
Type: appconfig.InstallerTypePacman,
}
assertNoValidationErrors(t, newTestPacmanInstaller(validPacmanData).Validate())
// Valid yay installer
validYayData := &appconfig.InstallerData{
Name: strPtr("visual-studio-code-bin"),
Type: appconfig.InstallerTypeYay,
}
assertNoValidationErrors(t, newTestYayInstaller(validYayData).Validate())
// Invalid: nil name
nilNameData := &appconfig.InstallerData{
Name: nil,
Type: appconfig.InstallerTypePacman,
}
assertValidationError(t, newTestPacmanInstaller(nilNameData).Validate(), "name")
}
func TestPacmanGetBinName(t *testing.T) {
logger.InitLogger(false)
// Test default bin name (uses package name)
defaultBinData := &appconfig.InstallerData{
Name: strPtr("neovim"),
Type: appconfig.InstallerTypePacman,
}
installer := newTestPacmanInstaller(defaultBinData)
if installer.GetBinName() != "neovim" {
t.Errorf("expected bin name 'neovim', got '%s'", installer.GetBinName())
}
// Test custom bin name
customBinName := "nvim"
customBinData := &appconfig.InstallerData{
Name: strPtr("neovim"),
Type: appconfig.InstallerTypePacman,
BinName: &customBinName,
}
installerWithCustomBin := newTestPacmanInstaller(customBinData)
if installerWithCustomBin.GetBinName() != "nvim" {
t.Errorf("expected bin name 'nvim', got '%s'", installerWithCustomBin.GetBinName())
}
}
func TestPacmanGetOpts(t *testing.T) {
logger.InitLogger(false)
// Test default opts (no options set)
defaultData := &appconfig.InstallerData{
Name: strPtr("vim"),
Type: appconfig.InstallerTypePacman,
}
installer := newTestPacmanInstaller(defaultData)
opts := installer.GetOpts()
if opts.Needed != nil {
t.Errorf("expected Needed to be nil, got %v", *opts.Needed)
}
// Test with needed option set to true
neededData := &appconfig.InstallerData{
Name: strPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"needed": true,
},
}
installerWithNeeded := newTestPacmanInstaller(neededData)
optsWithNeeded := installerWithNeeded.GetOpts()
if optsWithNeeded.Needed == nil || !*optsWithNeeded.Needed {
t.Errorf("expected Needed to be true")
}
// Test with needed option set to false
notNeededData := &appconfig.InstallerData{
Name: strPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"needed": false,
},
}
installerNotNeeded := newTestPacmanInstaller(notNeededData)
optsNotNeeded := installerNotNeeded.GetOpts()
if optsNotNeeded.Needed == nil || *optsNotNeeded.Needed {
t.Errorf("expected Needed to be false")
}
}