1787 lines
48 KiB
Go
1787 lines
48 KiB
Go
package installer
|
|
|
|
import (
|
|
"container/heap"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"zsvo/pkg/deps"
|
|
"zsvo/pkg/errors"
|
|
"zsvo/pkg/packager"
|
|
"zsvo/pkg/types"
|
|
)
|
|
|
|
// Installer handles installing and removing packages.
|
|
type Installer struct {
|
|
rootDir string
|
|
pkgDB string
|
|
resolver *deps.DependencyResolver
|
|
repo *InstallerPackageRepository
|
|
}
|
|
|
|
// RemoveOptions controls package removal behavior.
|
|
type RemoveOptions struct {
|
|
Cascade bool
|
|
}
|
|
|
|
func NewInstaller(rootDir string) *Installer {
|
|
inst := &Installer{
|
|
rootDir: rootDir,
|
|
pkgDB: filepath.Join(rootDir, "var", "lib", "pkgdb"),
|
|
}
|
|
|
|
// Initialize repository and resolver with fetcher
|
|
inst.repo = NewInstallerPackageRepository(inst)
|
|
inst.resolver = deps.NewDependencyResolver(inst.repo, inst) // Installer implements PackageFetcher
|
|
|
|
return inst
|
|
}
|
|
|
|
// Install installs a single package archive produced by zsvo build.
|
|
func (i *Installer) Install(packagePath string) error {
|
|
return i.InstallMany([]string{packagePath})
|
|
}
|
|
|
|
// InstallMany installs multiple package archives in one transaction.
|
|
func (i *Installer) InstallMany(packagePaths []string) error {
|
|
if len(packagePaths) == 0 {
|
|
return fmt.Errorf("no packages provided")
|
|
}
|
|
|
|
return i.withDBLock(true, func() error {
|
|
return i.installManyNoLock(packagePaths)
|
|
})
|
|
}
|
|
|
|
// InstallWithAutoDeps installs packages with automatic dependency resolution
|
|
func (i *Installer) InstallWithAutoDeps(packagePaths []string) error {
|
|
if len(packagePaths) == 0 {
|
|
return fmt.Errorf("no packages provided")
|
|
}
|
|
|
|
return i.withDBLock(true, func() error {
|
|
// Read all package candidates
|
|
candidates, err := i.readInstallCandidatesNoLock(packagePaths)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Resolve all dependencies recursively
|
|
allPackages, err := i.resolveAllDependencies(candidates)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert to package paths
|
|
finalPaths := make([]string, 0, len(allPackages))
|
|
for _, pkg := range allPackages {
|
|
finalPaths = append(finalPaths, pkg.path)
|
|
}
|
|
|
|
// Install all packages in correct order
|
|
return i.installManyNoLock(finalPaths)
|
|
})
|
|
}
|
|
|
|
// Upgrade upgrades packages from local package files.
|
|
func (i *Installer) Upgrade(packagePaths []string) error {
|
|
return i.InstallMany(packagePaths)
|
|
}
|
|
|
|
// PlanRemove returns removal plan without changing filesystem.
|
|
func (i *Installer) PlanRemove(packageNames []string, options RemoveOptions) ([]string, error) {
|
|
var plan []string
|
|
if err := i.withDBLock(false, func() error {
|
|
var err error
|
|
plan, err = i.planRemoveNoLock(packageNames, options)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return plan, nil
|
|
}
|
|
|
|
// Remove removes a single installed package.
|
|
func (i *Installer) Remove(packageName string) error {
|
|
_, err := i.RemoveMany([]string{packageName}, RemoveOptions{})
|
|
return err
|
|
}
|
|
|
|
// RemoveMany removes one or more installed packages in one transaction.
|
|
func (i *Installer) RemoveMany(packageNames []string, options RemoveOptions) ([]string, error) {
|
|
var removed []string
|
|
if err := i.withDBLock(true, func() error {
|
|
plan, err := i.planRemoveNoLock(packageNames, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
removed, err = i.removeManyWithPlanNoLock(plan)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return removed, nil
|
|
}
|
|
|
|
// ListInstalled lists all installed packages.
|
|
func (i *Installer) ListInstalled() ([]string, error) {
|
|
var packages []string
|
|
if err := i.withDBLock(false, func() error {
|
|
var err error
|
|
packages, err = i.listInstalledNoLock()
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return packages, nil
|
|
}
|
|
|
|
// ListOrphans returns installed packages that are not required by others.
|
|
func (i *Installer) ListOrphans() ([]string, error) {
|
|
var orphans []string
|
|
if err := i.withDBLock(false, func() error {
|
|
infos, err := i.installedPkgInfosNoLock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
orphans = listOrphansFromInfos(infos)
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return orphans, nil
|
|
}
|
|
|
|
// GetPackageInfo returns metadata for an installed package.
|
|
func (i *Installer) GetPackageInfo(packageName string) (*types.PkgInfo, error) {
|
|
var pkgInfo *types.PkgInfo
|
|
if err := i.withDBLock(false, func() error {
|
|
var err error
|
|
pkgInfo, err = i.getPackageInfoNoLock(packageName)
|
|
return err
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pkgInfo, nil
|
|
}
|
|
|
|
func (i *Installer) installManyNoLock(packagePaths []string) error {
|
|
candidates, err := i.readInstallCandidatesNoLock(packagePaths)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
installedInfos, err := i.installedPkgInfosNoLock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txDir, err := os.MkdirTemp("", "install-many-")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create transaction directory: %w", err)
|
|
}
|
|
defer os.RemoveAll(txDir)
|
|
|
|
applied := make([]installTransactionState, 0, len(candidates))
|
|
ordered, err := solveInstallOrder(candidates, installedInfos)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, candidate := range ordered {
|
|
state, err := i.installPackageTxNoLock(candidate.path, txDir, installedInfos)
|
|
if err != nil {
|
|
if rbErr := i.rollbackInstallTransactionNoLock(applied); rbErr != nil {
|
|
return fmt.Errorf("install failed for %s: %w (rollback error: %v)", candidate.info.Name, err, rbErr)
|
|
}
|
|
return fmt.Errorf("install failed for %s: %w", candidate.info.Name, err)
|
|
}
|
|
|
|
applied = append(applied, *state)
|
|
installedInfos[candidate.info.Name] = candidate.info
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *Installer) readInstallCandidatesNoLock(packagePaths []string) ([]installCandidate, error) {
|
|
p := packager.NewPackager(i.rootDir)
|
|
|
|
candidates := make([]installCandidate, 0, len(packagePaths))
|
|
seenNames := make(map[string]string, len(packagePaths))
|
|
for _, packagePath := range packagePaths {
|
|
if strings.TrimSpace(packagePath) == "" {
|
|
return nil, fmt.Errorf("package path cannot be empty")
|
|
}
|
|
|
|
pkgInfo, err := p.ReadPkgInfo(packagePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read package metadata from %s: %w", packagePath, err)
|
|
}
|
|
if err := validateSimpleDependencies(pkgInfo.Dependencies); err != nil {
|
|
return nil, fmt.Errorf("invalid dependencies in %s: %w", packagePath, err)
|
|
}
|
|
|
|
if prev, exists := seenNames[pkgInfo.Name]; exists {
|
|
return nil, fmt.Errorf("duplicate package %s in transaction: %s and %s", pkgInfo.Name, prev, packagePath)
|
|
}
|
|
seenNames[pkgInfo.Name] = packagePath
|
|
|
|
candidates = append(candidates, installCandidate{path: packagePath, info: pkgInfo})
|
|
}
|
|
|
|
return candidates, nil
|
|
}
|
|
|
|
func (i *Installer) installPackageTxNoLock(packagePath, txDir string, installedInfos map[string]*types.PkgInfo) (*installTransactionState, error) {
|
|
if packagePath == "" {
|
|
return nil, fmt.Errorf("package path cannot be empty")
|
|
}
|
|
if txDir == "" {
|
|
return nil, fmt.Errorf("transaction directory cannot be empty")
|
|
}
|
|
if installedInfos == nil {
|
|
return nil, fmt.Errorf("installed infos map cannot be nil")
|
|
}
|
|
|
|
pkgTxDir, err := os.MkdirTemp(txDir, "pkg-")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create package transaction directory: %w", err)
|
|
}
|
|
|
|
extractDir := filepath.Join(pkgTxDir, "extract")
|
|
p := packager.NewPackager(i.rootDir)
|
|
if err := p.Extract(packagePath, extractDir); err != nil {
|
|
return nil, fmt.Errorf("failed to extract package: %w", err)
|
|
}
|
|
|
|
pkgInfo, err := p.ReadPkgInfo(packagePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read package metadata: %w", err)
|
|
}
|
|
if pkgInfo == nil {
|
|
return nil, fmt.Errorf("package metadata is nil")
|
|
}
|
|
if err := validateSimpleDependencies(pkgInfo.Dependencies); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := checkDependenciesAgainstInstalled(pkgInfo.Dependencies, installedInfos, pkgInfo.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := i.checkFileOwnershipNoLock(pkgInfo, installedInfos); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
extractRoot, err := i.resolveExtractRoot(extractDir, pkgInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve package root: %w", err)
|
|
}
|
|
|
|
state := &installTransactionState{pkgInfo: pkgInfo, txDir: pkgTxDir}
|
|
|
|
// Same-name install is treated as upgrade/reinstall.
|
|
if oldInfo, exists := installedInfos[pkgInfo.Name]; exists {
|
|
replacedBackupRoot := filepath.Join(pkgTxDir, "replaced")
|
|
backups, err := i.removeFiles(oldInfo, replacedBackupRoot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to remove old package %s files: %w", pkgInfo.Name, err)
|
|
}
|
|
if err := i.unregisterPackage(pkgInfo.Name); err != nil {
|
|
_ = i.rollbackRemove(backups)
|
|
return nil, fmt.Errorf("failed to unregister old package %s: %w", pkgInfo.Name, err)
|
|
}
|
|
|
|
state.replaced = &removeTransactionState{
|
|
pkgName: pkgInfo.Name,
|
|
info: oldInfo,
|
|
backups: backups,
|
|
}
|
|
}
|
|
|
|
backupRoot := filepath.Join(pkgTxDir, "new")
|
|
installedPaths, backups, err := i.installFiles(extractRoot, pkgInfo, backupRoot)
|
|
if err != nil {
|
|
_ = i.rollbackInstall(installedPaths, backups)
|
|
if state.replaced != nil {
|
|
_ = i.rollbackRemove(state.replaced.backups)
|
|
_ = i.registerPackage(state.replaced.info)
|
|
}
|
|
return nil, fmt.Errorf("failed to install files: %w", err)
|
|
}
|
|
state.installedPaths = installedPaths
|
|
state.newBackups = backups
|
|
|
|
if err := i.registerPackage(pkgInfo); err != nil {
|
|
_ = i.rollbackInstall(installedPaths, backups)
|
|
if state.replaced != nil {
|
|
_ = i.rollbackRemove(state.replaced.backups)
|
|
_ = i.registerPackage(state.replaced.info)
|
|
}
|
|
return nil, fmt.Errorf("failed to register package: %w", err)
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
func (i *Installer) rollbackInstallTransactionNoLock(applied []installTransactionState) error {
|
|
if len(applied) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var errs []string
|
|
for idx := len(applied) - 1; idx >= 0; idx-- {
|
|
state := applied[idx]
|
|
|
|
rollbackRoot := filepath.Join(state.txDir, "rollback-new")
|
|
backups, err := i.removeFiles(state.pkgInfo, rollbackRoot)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("failed to remove %s during rollback: %v", state.pkgInfo.Name, err))
|
|
continue
|
|
}
|
|
if err := i.unregisterPackage(state.pkgInfo.Name); err != nil {
|
|
_ = i.rollbackRemove(backups)
|
|
errs = append(errs, fmt.Sprintf("failed to unregister %s during rollback: %v", state.pkgInfo.Name, err))
|
|
continue
|
|
}
|
|
|
|
if err := i.rollbackInstall(state.installedPaths, state.newBackups); err != nil {
|
|
errs = append(errs, fmt.Sprintf("failed to rollback overwritten paths for %s: %v", state.pkgInfo.Name, err))
|
|
}
|
|
|
|
if state.replaced != nil {
|
|
if err := i.rollbackRemove(state.replaced.backups); err != nil {
|
|
errs = append(errs, fmt.Sprintf("failed to restore old files for %s: %v", state.replaced.pkgName, err))
|
|
}
|
|
if err := i.registerPackage(state.replaced.info); err != nil {
|
|
errs = append(errs, fmt.Sprintf("failed to re-register %s: %v", state.replaced.pkgName, err))
|
|
}
|
|
}
|
|
|
|
// Clean up transaction directory
|
|
if state.txDir != "" {
|
|
if err := os.RemoveAll(state.txDir); err != nil {
|
|
errs = append(errs, fmt.Sprintf("failed to cleanup transaction directory for %s: %v", state.pkgInfo.Name, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("install transaction rollback failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *Installer) planRemoveNoLock(packageNames []string, options RemoveOptions) ([]string, error) {
|
|
names, err := normalizePackageNames(packageNames)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
infos, err := i.installedPkgInfosNoLock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
removeSet := make(map[string]struct{}, len(names))
|
|
for _, name := range names {
|
|
if _, ok := infos[name]; !ok {
|
|
return nil, fmt.Errorf("package %s not found in database", name)
|
|
}
|
|
removeSet[name] = struct{}{}
|
|
}
|
|
|
|
if options.Cascade {
|
|
for {
|
|
broken, err := brokenPackagesAfterRemoval(infos, removeSet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
changed := false
|
|
for _, name := range broken {
|
|
if _, exists := removeSet[name]; exists {
|
|
continue
|
|
}
|
|
removeSet[name] = struct{}{}
|
|
changed = true
|
|
}
|
|
|
|
if !changed {
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
broken, err := brokenPackagesAfterRemoval(infos, removeSet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(broken) > 0 {
|
|
return nil, fmt.Errorf("cannot remove %s: required by %s", strings.Join(names, ", "), strings.Join(broken, ", "))
|
|
}
|
|
}
|
|
|
|
plan := make([]string, 0, len(removeSet))
|
|
for name := range removeSet {
|
|
plan = append(plan, name)
|
|
}
|
|
sort.Strings(plan)
|
|
return plan, nil
|
|
}
|
|
|
|
func (i *Installer) removeManyWithPlanNoLock(plan []string) ([]string, error) {
|
|
if len(plan) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
txDir, err := os.MkdirTemp("", "remove-many-")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create remove transaction directory: %w", err)
|
|
}
|
|
defer os.RemoveAll(txDir)
|
|
|
|
states := make([]removeTransactionState, 0, len(plan))
|
|
for _, packageName := range plan {
|
|
state, err := i.removePackageTxNoLock(packageName, txDir)
|
|
if err != nil {
|
|
if rbErr := i.rollbackRemoveTransactionNoLock(states); rbErr != nil {
|
|
return nil, fmt.Errorf("failed to remove package %s: %w (rollback error: %v)", packageName, err, rbErr)
|
|
}
|
|
return nil, fmt.Errorf("failed to remove package %s: %w", packageName, err)
|
|
}
|
|
states = append(states, *state)
|
|
}
|
|
|
|
return append([]string(nil), plan...), nil
|
|
}
|
|
|
|
func (i *Installer) removePackageTxNoLock(packageName, txDir string) (*removeTransactionState, error) {
|
|
pkgInfo, err := i.getPackageInfoNoLock(packageName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("package not found: %w", err)
|
|
}
|
|
|
|
backupRoot := filepath.Join(txDir, packageName)
|
|
backups, err := i.removeFiles(pkgInfo, backupRoot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to remove files: %w", err)
|
|
}
|
|
if err := i.unregisterPackage(packageName); err != nil {
|
|
_ = i.rollbackRemove(backups)
|
|
return nil, fmt.Errorf("failed to unregister package: %w", err)
|
|
}
|
|
|
|
return &removeTransactionState{pkgName: packageName, info: pkgInfo, backups: backups}, nil
|
|
}
|
|
|
|
func (i *Installer) rollbackRemoveTransactionNoLock(states []removeTransactionState) error {
|
|
if len(states) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var errs []string
|
|
for idx := len(states) - 1; idx >= 0; idx-- {
|
|
state := states[idx]
|
|
if err := i.rollbackRemove(state.backups); err != nil {
|
|
errs = append(errs, err.Error())
|
|
}
|
|
if err := i.registerPackage(state.info); err != nil {
|
|
errs = append(errs, fmt.Sprintf("failed to re-register %s: %v", state.pkgName, err))
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("remove transaction rollback failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *Installer) checkFileOwnershipNoLock(pkgInfo *types.PkgInfo, installedInfos map[string]*types.PkgInfo) error {
|
|
ownerByPath := make(map[string]string)
|
|
for ownerName, info := range installedInfos {
|
|
for _, file := range info.Files {
|
|
relPath, err := sanitizePackagePath(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ownerByPath[relPath] = ownerName
|
|
}
|
|
}
|
|
|
|
rootAbs, err := filepath.Abs(filepath.Clean(i.rootDir))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve root directory: %w", err)
|
|
}
|
|
|
|
for _, file := range pkgInfo.Files {
|
|
relPath, err := sanitizePackagePath(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if owner, exists := ownerByPath[relPath]; exists && owner != pkgInfo.Name {
|
|
return fmt.Errorf("file conflict: %s is owned by %s", relPath, owner)
|
|
}
|
|
|
|
dst, err := safeJoinRoot(rootAbs, relPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := os.Lstat(dst); err == nil {
|
|
if _, owned := ownerByPath[relPath]; !owned {
|
|
return fmt.Errorf("file conflict: %s exists in filesystem and is not owned by any package", relPath)
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *Installer) installFiles(extractRoot string, pkgInfo *types.PkgInfo, backupRoot string) ([]string, []pathBackup, error) {
|
|
extractRoot = filepath.Clean(extractRoot)
|
|
rootAbs, err := filepath.Abs(filepath.Clean(i.rootDir))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to resolve root directory: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(rootAbs, 0o755); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create root directory %s: %w", rootAbs, err)
|
|
}
|
|
|
|
installedPaths := make([]string, 0, len(pkgInfo.Files))
|
|
backups := make([]pathBackup, 0, len(pkgInfo.Files))
|
|
for _, file := range pkgInfo.Files {
|
|
relPath, err := sanitizePackagePath(file)
|
|
if err != nil {
|
|
return installedPaths, backups, err
|
|
}
|
|
|
|
src := filepath.Join(extractRoot, relPath)
|
|
dst, err := safeJoinRoot(rootAbs, relPath)
|
|
if err != nil {
|
|
return installedPaths, backups, err
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return installedPaths, backups, fmt.Errorf("failed to create directory %s: %w", filepath.Dir(dst), err)
|
|
}
|
|
|
|
if _, err := os.Lstat(dst); err == nil {
|
|
backupPath := filepath.Join(backupRoot, relPath)
|
|
if err := os.MkdirAll(filepath.Dir(backupPath), 0o755); err != nil {
|
|
return installedPaths, backups, err
|
|
}
|
|
if err := i.clonePath(dst, backupPath); err != nil {
|
|
return installedPaths, backups, fmt.Errorf("failed to backup %s: %w", relPath, err)
|
|
}
|
|
backups = append(backups, pathBackup{originalPath: dst, backupPath: backupPath})
|
|
} else if !os.IsNotExist(err) {
|
|
return installedPaths, backups, err
|
|
}
|
|
|
|
if err := i.clonePath(src, dst); err != nil {
|
|
return installedPaths, backups, fmt.Errorf("failed to copy %s: %w", relPath, err)
|
|
}
|
|
|
|
installedPaths = append(installedPaths, dst)
|
|
}
|
|
|
|
return installedPaths, backups, nil
|
|
}
|
|
|
|
func (i *Installer) rollbackInstall(installedPaths []string, backups []pathBackup) error {
|
|
var errs []string
|
|
|
|
seen := make(map[string]struct{}, len(installedPaths))
|
|
for idx := len(installedPaths) - 1; idx >= 0; idx-- {
|
|
path := installedPaths[idx]
|
|
if _, ok := seen[path]; ok {
|
|
continue
|
|
}
|
|
seen[path] = struct{}{}
|
|
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
errs = append(errs, fmt.Sprintf("remove %s: %v", path, err))
|
|
}
|
|
}
|
|
|
|
for idx := len(backups) - 1; idx >= 0; idx-- {
|
|
backup := backups[idx]
|
|
if err := os.MkdirAll(filepath.Dir(backup.originalPath), 0o755); err != nil {
|
|
errs = append(errs, fmt.Sprintf("mkdir %s: %v", filepath.Dir(backup.originalPath), err))
|
|
continue
|
|
}
|
|
if err := i.clonePath(backup.backupPath, backup.originalPath); err != nil {
|
|
errs = append(errs, fmt.Sprintf("restore %s: %v", backup.originalPath, err))
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("install rollback failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *Installer) resolveExtractRoot(tmpDir string, pkgInfo *types.PkgInfo) (string, error) {
|
|
if filesExistUnderRoot(tmpDir, pkgInfo.Files) {
|
|
return tmpDir, nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(tmpDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
candidate := filepath.Join(tmpDir, entry.Name())
|
|
if filesExistUnderRoot(candidate, pkgInfo.Files) {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("package archive layout does not match file list")
|
|
}
|
|
|
|
func filesExistUnderRoot(root string, files []string) bool {
|
|
for _, file := range files {
|
|
relPath, err := sanitizePackagePath(file)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if _, err := os.Lstat(filepath.Join(root, relPath)); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// removeFiles removes package files and stores backups for rollback.
|
|
func (i *Installer) removeFiles(pkgInfo *types.PkgInfo, backupRoot string) ([]pathBackup, error) {
|
|
rootAbs, err := filepath.Abs(filepath.Clean(i.rootDir))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve root directory: %w", err)
|
|
}
|
|
|
|
backups := make([]pathBackup, 0, len(pkgInfo.Files))
|
|
for _, file := range pkgInfo.Files {
|
|
relPath, err := sanitizePackagePath(file)
|
|
if err != nil {
|
|
return backups, err
|
|
}
|
|
|
|
path, err := safeJoinRoot(rootAbs, relPath)
|
|
if err != nil {
|
|
return backups, err
|
|
}
|
|
|
|
if _, err := os.Lstat(path); os.IsNotExist(err) {
|
|
continue
|
|
} else if err != nil {
|
|
return backups, err
|
|
}
|
|
|
|
backupPath := filepath.Join(backupRoot, relPath)
|
|
if err := os.MkdirAll(filepath.Dir(backupPath), 0o755); err != nil {
|
|
return backups, err
|
|
}
|
|
if err := i.clonePath(path, backupPath); err != nil {
|
|
return backups, fmt.Errorf("failed to backup %s: %w", relPath, err)
|
|
}
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return backups, fmt.Errorf("failed to remove %s: %w", relPath, err)
|
|
}
|
|
|
|
backups = append(backups, pathBackup{originalPath: path, backupPath: backupPath})
|
|
}
|
|
|
|
return backups, nil
|
|
}
|
|
|
|
func (i *Installer) rollbackRemove(backups []pathBackup) error {
|
|
var errs []string
|
|
|
|
for idx := len(backups) - 1; idx >= 0; idx-- {
|
|
backup := backups[idx]
|
|
if err := os.MkdirAll(filepath.Dir(backup.originalPath), 0o755); err != nil {
|
|
errs = append(errs, fmt.Sprintf("mkdir %s: %v", filepath.Dir(backup.originalPath), err))
|
|
continue
|
|
}
|
|
if err := i.clonePath(backup.backupPath, backup.originalPath); err != nil {
|
|
errs = append(errs, fmt.Sprintf("restore %s: %v", backup.originalPath, err))
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("remove rollback failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *Installer) registerPackage(pkgInfo *types.PkgInfo) error {
|
|
if err := pkgInfo.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := validateSimpleDependencies(pkgInfo.Dependencies); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.MkdirAll(i.pkgDB, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create package database: %w", err)
|
|
}
|
|
|
|
pkgDir := filepath.Join(i.pkgDB, pkgInfo.Name)
|
|
if err := os.MkdirAll(pkgDir, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create package database directory: %w", err)
|
|
}
|
|
|
|
pkgInfoFile := filepath.Join(pkgDir, types.PackageMetadataFile)
|
|
file, err := os.Create(pkgInfoFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
return types.WritePkgInfo(file, pkgInfo)
|
|
}
|
|
|
|
func (i *Installer) unregisterPackage(packageName string) error {
|
|
return os.RemoveAll(filepath.Join(i.pkgDB, packageName))
|
|
}
|
|
|
|
func (i *Installer) getPackageInfoNoLock(packageName string) (*types.PkgInfo, error) {
|
|
pkgDir := filepath.Join(i.pkgDB, packageName)
|
|
pkgInfoFile := filepath.Join(pkgDir, types.PackageMetadataFile)
|
|
|
|
if _, err := os.Stat(pkgDir); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("package %s not found in database", packageName)
|
|
}
|
|
if _, err := os.Stat(pkgInfoFile); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("package metadata file not found for %s", packageName)
|
|
}
|
|
|
|
file, err := os.Open(pkgInfoFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
pkgInfo, err := types.ReadPkgInfo(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid package metadata for %s: %w", packageName, err)
|
|
}
|
|
|
|
return pkgInfo, nil
|
|
}
|
|
|
|
func (i *Installer) listInstalledNoLock() ([]string, error) {
|
|
var packages []string
|
|
|
|
if _, err := os.Stat(i.pkgDB); os.IsNotExist(err) {
|
|
return packages, nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(i.pkgDB)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read package database: %w", err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
packages = append(packages, entry.Name())
|
|
}
|
|
}
|
|
sort.Strings(packages)
|
|
return packages, nil
|
|
}
|
|
|
|
func (i *Installer) installedPkgInfosNoLock() (map[string]*types.PkgInfo, error) {
|
|
installed, err := i.listInstalledNoLock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
infos := make(map[string]*types.PkgInfo, len(installed))
|
|
for _, pkgName := range installed {
|
|
info, err := i.getPackageInfoNoLock(pkgName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
infos[pkgName] = info
|
|
}
|
|
|
|
return infos, nil
|
|
}
|
|
|
|
func (i *Installer) clonePath(src, dst string) error {
|
|
info, err := os.Lstat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case info.Mode()&os.ModeSymlink != 0:
|
|
target, err := os.Readlink(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := removePathIfExists(dst); err != nil {
|
|
return err
|
|
}
|
|
return os.Symlink(target, dst)
|
|
case info.IsDir():
|
|
return os.MkdirAll(dst, info.Mode().Perm())
|
|
case info.Mode().IsRegular():
|
|
sourceFile, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
destFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode().Perm())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
if _, err := io.Copy(destFile, sourceFile); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported file type: %s", src)
|
|
}
|
|
}
|
|
|
|
func removePathIfExists(path string) error {
|
|
if err := os.RemoveAll(path); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSimpleDependencies(rawDeps []string) error {
|
|
if rawDeps == nil {
|
|
return nil // Empty dependencies are valid
|
|
}
|
|
if _, err := deps.ParseRequirements(rawDeps); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func depsSatisfied(rawDeps []string, installedInfos map[string]*types.PkgInfo, selfName string) bool {
|
|
reqs, err := deps.ParseRequirements(rawDeps)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
available := packageVersions(installedInfos)
|
|
for _, req := range reqs {
|
|
if _, ok := resolveRequirementProvider(req, available, nil, selfName); !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func missingDeps(rawDeps []string, installedInfos map[string]*types.PkgInfo, selfName string) []string {
|
|
reqs, err := deps.ParseRequirements(rawDeps)
|
|
if err != nil {
|
|
return []string{err.Error()}
|
|
}
|
|
|
|
missing := make([]string, 0)
|
|
seen := make(map[string]struct{})
|
|
available := packageVersions(installedInfos)
|
|
for _, req := range reqs {
|
|
if _, ok := resolveRequirementProvider(req, available, nil, selfName); ok {
|
|
continue
|
|
}
|
|
label := req.Raw
|
|
if label == "" {
|
|
label = formatRequirement(req)
|
|
}
|
|
if _, exists := seen[label]; exists {
|
|
continue
|
|
}
|
|
seen[label] = struct{}{}
|
|
missing = append(missing, label)
|
|
}
|
|
sort.Strings(missing)
|
|
return missing
|
|
}
|
|
|
|
func checkDependenciesAgainstInstalled(deps []string, installedInfos map[string]*types.PkgInfo, selfName string) error {
|
|
if depsSatisfied(deps, installedInfos, selfName) {
|
|
return nil
|
|
}
|
|
missing := missingDeps(deps, installedInfos, selfName)
|
|
if len(missing) == 0 {
|
|
return fmt.Errorf("dependency check failed")
|
|
}
|
|
return fmt.Errorf("dependency not installed: %s", strings.Join(missing, ", "))
|
|
}
|
|
|
|
func normalizePackageNames(packageNames []string) ([]string, error) {
|
|
if len(packageNames) == 0 {
|
|
return nil, fmt.Errorf("no packages provided")
|
|
}
|
|
|
|
normalized := make([]string, 0, len(packageNames))
|
|
seen := make(map[string]struct{}, len(packageNames))
|
|
for _, packageName := range packageNames {
|
|
name := strings.TrimSpace(packageName)
|
|
if name == "" {
|
|
return nil, fmt.Errorf("package name cannot be empty")
|
|
}
|
|
if _, exists := seen[name]; exists {
|
|
return nil, fmt.Errorf("duplicate package in request: %s", name)
|
|
}
|
|
seen[name] = struct{}{}
|
|
normalized = append(normalized, name)
|
|
}
|
|
|
|
sort.Strings(normalized)
|
|
return normalized, nil
|
|
}
|
|
|
|
func brokenPackagesAfterRemoval(infos map[string]*types.PkgInfo, removeSet map[string]struct{}) ([]string, error) {
|
|
remaining := make(map[string]*types.PkgInfo, len(infos))
|
|
for name, info := range infos {
|
|
if _, removed := removeSet[name]; removed {
|
|
continue
|
|
}
|
|
remaining[name] = info
|
|
}
|
|
|
|
names := make([]string, 0, len(remaining))
|
|
for name := range remaining {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
broken := make([]string, 0)
|
|
remainingVersions := packageVersions(remaining)
|
|
for _, pkgName := range names {
|
|
pkgInfo := remaining[pkgName]
|
|
reqs, err := deps.ParseRequirements(pkgInfo.Dependencies)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid dependency in package %s: %w", pkgName, err)
|
|
}
|
|
for _, req := range reqs {
|
|
if _, ok := resolveRequirementProvider(req, remainingVersions, nil, pkgName); !ok {
|
|
broken = append(broken, pkgName)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return broken, nil
|
|
}
|
|
|
|
func listOrphansFromInfos(infos map[string]*types.PkgInfo) []string {
|
|
required := make(map[string]struct{})
|
|
versions := packageVersions(infos)
|
|
for owner, info := range infos {
|
|
reqs, err := deps.ParseRequirements(info.Dependencies)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, req := range reqs {
|
|
provider, ok := resolveRequirementProvider(req, versions, nil, owner)
|
|
if !ok || provider == owner {
|
|
continue
|
|
}
|
|
required[provider] = struct{}{}
|
|
}
|
|
}
|
|
|
|
orphans := make([]string, 0)
|
|
for name := range infos {
|
|
if _, ok := required[name]; !ok {
|
|
orphans = append(orphans, name)
|
|
}
|
|
}
|
|
sort.Strings(orphans)
|
|
return orphans
|
|
}
|
|
|
|
func solveInstallOrder(candidates []installCandidate, installedInfos map[string]*types.PkgInfo) ([]installCandidate, error) {
|
|
if len(candidates) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
candidateByName := make(map[string]installCandidate, len(candidates))
|
|
for _, candidate := range candidates {
|
|
if candidate.info == nil {
|
|
return nil, fmt.Errorf("invalid install candidate: nil metadata")
|
|
}
|
|
if prev, exists := candidateByName[candidate.info.Name]; exists {
|
|
return nil, fmt.Errorf("duplicate package %s in transaction: %s and %s", candidate.info.Name, prev.path, candidate.path)
|
|
}
|
|
candidateByName[candidate.info.Name] = candidate
|
|
}
|
|
|
|
available := packageVersions(installedInfos)
|
|
for _, candidate := range candidates {
|
|
available[candidate.info.Name] = candidate.info.Version
|
|
}
|
|
|
|
indegree := make(map[string]int, len(candidates))
|
|
dependents := make(map[string][]string, len(candidates))
|
|
for _, candidate := range candidates {
|
|
indegree[candidate.info.Name] = 0
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
reqs, err := deps.ParseRequirements(candidate.info.Dependencies)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid dependencies in %s: %w", candidate.info.Name, err)
|
|
}
|
|
|
|
edgeSet := make(map[string]struct{})
|
|
for _, req := range reqs {
|
|
provider, ok := resolveRequirementProvider(req, available, candidateByName, candidate.info.Name)
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot resolve dependencies for %s: missing %s", candidate.info.Name, req.Raw)
|
|
}
|
|
if provider == candidate.info.Name {
|
|
continue
|
|
}
|
|
if _, inTx := candidateByName[provider]; !inTx {
|
|
continue
|
|
}
|
|
if _, seen := edgeSet[provider]; seen {
|
|
continue
|
|
}
|
|
edgeSet[provider] = struct{}{}
|
|
dependents[provider] = append(dependents[provider], candidate.info.Name)
|
|
indegree[candidate.info.Name]++
|
|
}
|
|
}
|
|
|
|
ready := &stringMinHeap{}
|
|
heap.Init(ready)
|
|
for name, deg := range indegree {
|
|
if deg == 0 {
|
|
heap.Push(ready, name)
|
|
}
|
|
}
|
|
|
|
ordered := make([]installCandidate, 0, len(candidates))
|
|
for ready.Len() > 0 {
|
|
name := heap.Pop(ready).(string)
|
|
ordered = append(ordered, candidateByName[name])
|
|
|
|
next := dependents[name]
|
|
sort.Strings(next)
|
|
for _, depName := range next {
|
|
indegree[depName]--
|
|
if indegree[depName] == 0 {
|
|
heap.Push(ready, depName)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(ordered) != len(candidates) {
|
|
stuck := make([]string, 0)
|
|
for name, deg := range indegree {
|
|
if deg > 0 {
|
|
stuck = append(stuck, name)
|
|
}
|
|
}
|
|
sort.Strings(stuck)
|
|
return nil, fmt.Errorf("cannot resolve install order: dependency cycle among %s", strings.Join(stuck, ", "))
|
|
}
|
|
|
|
return ordered, nil
|
|
}
|
|
|
|
func packageVersions(infos map[string]*types.PkgInfo) map[string]string {
|
|
out := make(map[string]string, len(infos))
|
|
for name, info := range infos {
|
|
if info == nil {
|
|
continue
|
|
}
|
|
out[name] = info.Version
|
|
}
|
|
return out
|
|
}
|
|
|
|
func resolveRequirementProvider(
|
|
req deps.Requirement,
|
|
availableVersions map[string]string,
|
|
candidateByName map[string]installCandidate,
|
|
selfName string,
|
|
) (string, bool) {
|
|
for _, alt := range req.Alternatives {
|
|
version, ok := availableVersions[alt.Name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if alt.Name == selfName && candidateByName != nil {
|
|
if selfCandidate, exists := candidateByName[selfName]; exists {
|
|
version = selfCandidate.info.Version
|
|
}
|
|
}
|
|
if !alt.MatchesVersion(version) {
|
|
continue
|
|
}
|
|
|
|
// If a package with this name is in current transaction, it replaces installed one.
|
|
if candidateByName != nil {
|
|
if candidate, exists := candidateByName[alt.Name]; exists {
|
|
if alt.MatchesVersion(candidate.info.Version) {
|
|
return candidate.info.Name, true
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
return alt.Name, true
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func formatRequirement(req deps.Requirement) string {
|
|
if strings.TrimSpace(req.Raw) != "" {
|
|
return strings.TrimSpace(req.Raw)
|
|
}
|
|
parts := make([]string, 0, len(req.Alternatives))
|
|
for _, alt := range req.Alternatives {
|
|
if alt.Op == deps.OpAny {
|
|
parts = append(parts, alt.Name)
|
|
continue
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%s%s%s", alt.Name, formatOp(alt.Op), alt.Version))
|
|
}
|
|
return strings.Join(parts, " | ")
|
|
}
|
|
|
|
func formatOp(op deps.VersionOp) string {
|
|
switch op {
|
|
case deps.OpEqual:
|
|
return "="
|
|
case deps.OpGreater:
|
|
return ">"
|
|
case deps.OpGreaterOrEqual:
|
|
return ">="
|
|
case deps.OpLess:
|
|
return "<"
|
|
case deps.OpLessOrEqual:
|
|
return "<="
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
type stringMinHeap []string
|
|
|
|
func (h stringMinHeap) Len() int { return len(h) }
|
|
func (h stringMinHeap) Less(i, j int) bool { return h[i] < h[j] }
|
|
func (h stringMinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
|
func (h *stringMinHeap) Push(x interface{}) {
|
|
*h = append(*h, x.(string))
|
|
}
|
|
func (h *stringMinHeap) Pop() interface{} {
|
|
old := *h
|
|
last := len(old) - 1
|
|
item := old[last]
|
|
*h = old[:last]
|
|
return item
|
|
}
|
|
|
|
func sanitizePackagePath(path string) (string, error) {
|
|
if path == "" {
|
|
return "", fmt.Errorf("file path cannot be empty")
|
|
}
|
|
if filepath.IsAbs(path) {
|
|
return "", fmt.Errorf("absolute paths are not allowed in package: %s", path)
|
|
}
|
|
|
|
clean := filepath.Clean(path)
|
|
if clean == "." {
|
|
return "", fmt.Errorf("invalid file path: %s", path)
|
|
}
|
|
|
|
upPrefix := ".." + string(filepath.Separator)
|
|
if clean == ".." || strings.HasPrefix(clean, upPrefix) {
|
|
return "", fmt.Errorf("invalid file path: %s", path)
|
|
}
|
|
|
|
return clean, nil
|
|
}
|
|
|
|
func safeJoinRoot(rootAbs, relPath string) (string, error) {
|
|
if filepath.IsAbs(relPath) {
|
|
return "", fmt.Errorf("absolute path not allowed: %s", relPath)
|
|
}
|
|
|
|
joined := filepath.Clean(filepath.Join(rootAbs, relPath))
|
|
if rootAbs == string(filepath.Separator) {
|
|
return joined, nil
|
|
}
|
|
|
|
rootPrefix := rootAbs + string(filepath.Separator)
|
|
if joined != rootAbs && !strings.HasPrefix(joined, rootPrefix) {
|
|
return "", fmt.Errorf("path escapes root: %s", relPath)
|
|
}
|
|
|
|
return joined, nil
|
|
}
|
|
|
|
func (i *Installer) withDBLock(exclusive bool, fn func() error) error {
|
|
if _, err := os.Stat(i.pkgDB); os.IsNotExist(err) {
|
|
if !exclusive {
|
|
return fn()
|
|
}
|
|
if err := os.MkdirAll(i.pkgDB, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create package database: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
lockPath := filepath.Join(i.pkgDB, ".lock")
|
|
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open lock file: %w", err)
|
|
}
|
|
defer lockFile.Close()
|
|
|
|
lockType := syscall.LOCK_SH
|
|
if exclusive {
|
|
lockType = syscall.LOCK_EX
|
|
}
|
|
if err := syscall.Flock(int(lockFile.Fd()), lockType); err != nil {
|
|
return fmt.Errorf("failed to lock package database: %w", err)
|
|
}
|
|
defer syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
|
|
|
|
return fn()
|
|
}
|
|
|
|
// GetRootDir returns install root.
|
|
func (i *Installer) GetRootDir() string { return i.rootDir }
|
|
|
|
// GetPkgDB returns package database directory.
|
|
func (i *Installer) GetPkgDB() string { return i.pkgDB }
|
|
|
|
// IsInstalled checks whether package is installed.
|
|
func (i *Installer) IsInstalled(packageName string) bool {
|
|
_, err := os.Stat(filepath.Join(i.pkgDB, packageName))
|
|
return err == nil
|
|
}
|
|
|
|
// GetInstalledVersion returns installed version.
|
|
func (i *Installer) GetInstalledVersion(packageName string) (string, error) {
|
|
pkgInfo, err := i.GetPackageInfo(packageName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return pkgInfo.Version, nil
|
|
}
|
|
|
|
// GetInstalledFiles returns installed file list.
|
|
func (i *Installer) GetInstalledFiles(packageName string) ([]string, error) {
|
|
pkgInfo, err := i.GetPackageInfo(packageName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pkgInfo.Files, nil
|
|
}
|
|
|
|
// VerifyPackage checks that all package files exist.
|
|
func (i *Installer) VerifyPackage(packageName string) error {
|
|
pkgInfo, err := i.GetPackageInfo(packageName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rootAbs, err := filepath.Abs(filepath.Clean(i.rootDir))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve root directory: %w", err)
|
|
}
|
|
|
|
for _, file := range pkgInfo.Files {
|
|
relPath, err := sanitizePackagePath(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path, err := safeJoinRoot(rootAbs, relPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
return fmt.Errorf("missing file: %s", relPath)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetPackageSize returns total size of installed files.
|
|
func (i *Installer) GetPackageSize(packageName string) (int64, error) {
|
|
pkgInfo, err := i.GetPackageInfo(packageName)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
rootAbs, err := filepath.Abs(filepath.Clean(i.rootDir))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to resolve root directory: %w", err)
|
|
}
|
|
|
|
var totalSize int64
|
|
for _, file := range pkgInfo.Files {
|
|
relPath, err := sanitizePackagePath(file)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
path, err := safeJoinRoot(rootAbs, relPath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalSize += info.Size()
|
|
}
|
|
|
|
return totalSize, nil
|
|
}
|
|
|
|
// FetchPackage implements PackageFetcher interface
|
|
func (i *Installer) FetchPackage(pkgName string, version string) error {
|
|
// Try to find package in available search paths
|
|
searchPaths := []string{
|
|
filepath.Join(i.rootDir, "var", "cache", "packages"),
|
|
filepath.Join("/var", "cache", "packages"),
|
|
filepath.Join(i.rootDir, "work", "packages"),
|
|
}
|
|
|
|
for _, searchPath := range searchPaths {
|
|
if _, err := os.Stat(searchPath); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
// Search for package files
|
|
pkgPath, err := i.searchPackageInPath(pkgName, searchPath)
|
|
if err == nil {
|
|
fmt.Printf("Found package %s at %s\n", pkgName, pkgPath)
|
|
return nil // Package found and available
|
|
}
|
|
}
|
|
|
|
// If not found locally, try to download from remote sources
|
|
// This could integrate with Debian sources, custom repos, etc.
|
|
return fmt.Errorf("package %s not found locally and no remote fetcher configured", pkgName)
|
|
}
|
|
|
|
// searchPackageInPath searches for a package in a directory
|
|
func (i *Installer) searchPackageInPath(pkgName, searchPath string) (string, error) {
|
|
entries, err := os.ReadDir(searchPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(entry.Name(), pkgName) && strings.HasSuffix(entry.Name(), ".pkg.tar.zst") {
|
|
return filepath.Join(searchPath, entry.Name()), nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("package %s not found in %s", pkgName, searchPath)
|
|
}
|
|
|
|
// resolveAllDependencies recursively resolves all dependencies for packages using new resolver
|
|
func (i *Installer) resolveAllDependencies(initialCandidates []installCandidate) ([]installCandidate, error) {
|
|
// Convert candidates to deps.PackageInfo
|
|
packageInfos := make([]*deps.PackageInfo, len(initialCandidates))
|
|
for i, candidate := range initialCandidates {
|
|
packageInfos[i] = &deps.PackageInfo{
|
|
Name: candidate.info.Name,
|
|
Version: candidate.info.Version,
|
|
Dependencies: candidate.info.Dependencies,
|
|
}
|
|
}
|
|
|
|
// Use new resolver with recursive dependency resolution
|
|
resolved, err := i.resolver.ResolveDependencies(packageInfos)
|
|
if err != nil {
|
|
// Don't fail completely, just log warning and continue with what we have
|
|
fmt.Printf("Warning: dependency resolution failed: %v\n", err)
|
|
return initialCandidates, nil
|
|
}
|
|
|
|
// Convert back to installCandidate - need to find paths for resolved packages
|
|
result := make([]installCandidate, 0, len(resolved))
|
|
|
|
// First add original candidates
|
|
for _, candidate := range initialCandidates {
|
|
result = append(result, candidate)
|
|
}
|
|
|
|
// Then add any new dependencies that weren't in original list
|
|
for _, pkg := range resolved {
|
|
found := false
|
|
for _, candidate := range initialCandidates {
|
|
if candidate.info.Name == pkg.Name {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
// Try to find this package file
|
|
pkgPath, err := i.findPackageFile(pkg.Name)
|
|
if err != nil {
|
|
fmt.Printf("Warning: cannot find package file for %s: %v\n", pkg.Name, err)
|
|
continue
|
|
}
|
|
|
|
candidate, err := i.readInstallCandidatesNoLock([]string{pkgPath})
|
|
if err != nil {
|
|
fmt.Printf("Warning: cannot read package %s: %v\n", pkgPath, err)
|
|
continue
|
|
}
|
|
|
|
if len(candidate) > 0 {
|
|
result = append(result, candidate[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// findPackageFile finds package file by name
|
|
func (i *Installer) findPackageFile(pkgName string) (string, error) {
|
|
searchPaths := []string{
|
|
filepath.Join(i.rootDir, "var", "cache", "packages"),
|
|
filepath.Join("/var", "cache", "packages"),
|
|
filepath.Join(i.rootDir, "work", "packages"),
|
|
}
|
|
|
|
for _, searchPath := range searchPaths {
|
|
if _, err := os.Stat(searchPath); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
pkgPath, err := i.searchPackageInPath(pkgName, searchPath)
|
|
if err == nil {
|
|
return pkgPath, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("package file for %s not found", pkgName)
|
|
}
|
|
|
|
// findMissingDependencies finds dependencies that are not satisfied
|
|
func (i *Installer) findMissingDependencies(pkgInfo *types.PkgInfo, installed map[string]*deps.PackageInfo, candidates map[string]installCandidate) ([]string, error) {
|
|
var missing []string
|
|
|
|
reqs, err := deps.ParseRequirements(pkgInfo.Dependencies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, req := range reqs {
|
|
satisfied := false
|
|
|
|
// Check installed packages
|
|
for _, alt := range req.Alternatives {
|
|
if installedPkg, exists := installed[alt.Name]; exists {
|
|
if alt.MatchesVersion(installedPkg.Version) {
|
|
satisfied = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if satisfied {
|
|
continue
|
|
}
|
|
|
|
// Check candidates (packages we're about to install)
|
|
for _, alt := range req.Alternatives {
|
|
if candidate, exists := candidates[alt.Name]; exists {
|
|
if alt.MatchesVersion(candidate.info.Version) {
|
|
satisfied = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if satisfied {
|
|
continue
|
|
}
|
|
|
|
// If not satisfied by either, it's missing
|
|
missing = append(missing, req.Raw)
|
|
}
|
|
|
|
return missing, nil
|
|
}
|
|
|
|
// findDependencyPackage tries to find a dependency package in the system
|
|
func (i *Installer) findDependencyPackage(depName string) (installCandidate, error) {
|
|
// This is where you would implement package discovery
|
|
// For now, we'll return an error to indicate the dependency wasn't found
|
|
// In a real implementation, you might:
|
|
// 1. Search package repositories
|
|
// 2. Query package indexes
|
|
// 3. Use auto-build from source like in cmd/install.go
|
|
|
|
return installCandidate{}, errors.NewPackageNotFoundError(depName)
|
|
}
|
|
|
|
// InstallWithAutoResolve installs packages with automatic dependency resolution from available packages
|
|
func (i *Installer) InstallWithAutoResolve(packagePaths []string, searchPaths []string) error {
|
|
if len(packagePaths) == 0 {
|
|
return fmt.Errorf("no packages provided")
|
|
}
|
|
|
|
return i.withDBLock(true, func() error {
|
|
// Read all package candidates
|
|
candidates, err := i.readInstallCandidatesNoLock(packagePaths)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Resolve dependencies using available packages
|
|
allPackages, err := i.resolveDependenciesFromSearch(candidates, searchPaths)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert to package paths
|
|
finalPaths := make([]string, 0, len(allPackages))
|
|
for _, pkg := range allPackages {
|
|
finalPaths = append(finalPaths, pkg.path)
|
|
}
|
|
|
|
// Install all packages in correct order
|
|
return i.installManyNoLock(finalPaths)
|
|
})
|
|
}
|
|
|
|
// resolveDependenciesFromSearch resolves dependencies by searching in specified paths
|
|
func (i *Installer) resolveDependenciesFromSearch(initialCandidates []installCandidate, searchPaths []string) ([]installCandidate, error) {
|
|
// Build index of available packages from search paths
|
|
availablePackages, err := i.buildPackageIndex(searchPaths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
installed, err := i.installedPkgInfosNoLock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Track all packages to install
|
|
allPackages := make(map[string]installCandidate)
|
|
|
|
// Add initial packages
|
|
for _, candidate := range initialCandidates {
|
|
allPackages[candidate.info.Name] = candidate
|
|
}
|
|
|
|
// Process dependencies recursively
|
|
toProcess := make([]installCandidate, len(initialCandidates))
|
|
copy(toProcess, initialCandidates)
|
|
|
|
for len(toProcess) > 0 {
|
|
current := toProcess[0]
|
|
toProcess = toProcess[1:]
|
|
|
|
// Check dependencies
|
|
missing, err := i.findMissingDependenciesFromSearch(current.info, installed, allPackages, availablePackages)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add missing dependencies
|
|
for _, depName := range missing {
|
|
if _, exists := allPackages[depName]; exists {
|
|
continue // Already in our list
|
|
}
|
|
|
|
if depPkg, exists := availablePackages[depName]; exists {
|
|
allPackages[depName] = depPkg
|
|
toProcess = append(toProcess, depPkg)
|
|
} else {
|
|
return nil, errors.NewDependencyMissingError(current.info.Name, depName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve installation order
|
|
packageList := make([]*deps.PackageInfo, 0, len(allPackages))
|
|
for _, candidate := range allPackages {
|
|
packageList = append(packageList, &deps.PackageInfo{
|
|
Name: candidate.info.Name,
|
|
Version: candidate.info.Version,
|
|
Dependencies: candidate.info.Dependencies,
|
|
})
|
|
}
|
|
|
|
ordered, err := i.resolver.ResolveOrder(packageList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert back to installCandidate slice
|
|
result := make([]installCandidate, 0, len(ordered))
|
|
for _, pkg := range ordered {
|
|
result = append(result, allPackages[pkg.Name])
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// buildPackageIndex builds an index of available packages from search paths
|
|
func (i *Installer) buildPackageIndex(searchPaths []string) (map[string]installCandidate, error) {
|
|
index := make(map[string]installCandidate)
|
|
|
|
for _, searchPath := range searchPaths {
|
|
entries, err := os.ReadDir(searchPath)
|
|
if err != nil {
|
|
continue // Skip paths that don't exist or aren't readable
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !strings.HasSuffix(entry.Name(), ".pkg.tar.zst") {
|
|
continue
|
|
}
|
|
|
|
packagePath := filepath.Join(searchPath, entry.Name())
|
|
pkgInfo, err := i.readPackageInfo(packagePath)
|
|
if err != nil {
|
|
continue // Skip invalid packages
|
|
}
|
|
|
|
index[pkgInfo.Name] = installCandidate{
|
|
path: packagePath,
|
|
info: pkgInfo,
|
|
}
|
|
}
|
|
}
|
|
|
|
return index, nil
|
|
}
|
|
|
|
// findMissingDependenciesFromSearch finds missing dependencies by searching available packages
|
|
func (i *Installer) findMissingDependenciesFromSearch(pkgInfo *types.PkgInfo, installed map[string]*types.PkgInfo, candidates map[string]installCandidate, available map[string]installCandidate) ([]string, error) {
|
|
var missing []string
|
|
|
|
reqs, err := deps.ParseRequirements(pkgInfo.Dependencies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, req := range reqs {
|
|
satisfied := false
|
|
|
|
// Check installed packages
|
|
for _, alt := range req.Alternatives {
|
|
if installedPkg, exists := installed[alt.Name]; exists {
|
|
if alt.MatchesVersion(installedPkg.Version) {
|
|
satisfied = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if satisfied {
|
|
continue
|
|
}
|
|
|
|
// Check candidates
|
|
for _, alt := range req.Alternatives {
|
|
if candidate, exists := candidates[alt.Name]; exists {
|
|
if alt.MatchesVersion(candidate.info.Version) {
|
|
satisfied = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if satisfied {
|
|
continue
|
|
}
|
|
|
|
// Check available packages
|
|
for _, alt := range req.Alternatives {
|
|
if availablePkg, exists := available[alt.Name]; exists {
|
|
if alt.MatchesVersion(availablePkg.info.Version) {
|
|
satisfied = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if satisfied {
|
|
continue
|
|
}
|
|
|
|
// If not satisfied by any, it's missing
|
|
missing = append(missing, req.Raw)
|
|
}
|
|
|
|
return missing, nil
|
|
}
|
|
|
|
// readPackageInfo reads package info from a package file
|
|
func (i *Installer) readPackageInfo(packagePath string) (*types.PkgInfo, error) {
|
|
p := packager.NewPackager(i.rootDir)
|
|
return p.ReadPkgInfo(packagePath)
|
|
}
|
|
|
|
type installCandidate struct {
|
|
path string
|
|
info *types.PkgInfo
|
|
}
|
|
|
|
type installTransactionState struct {
|
|
pkgInfo *types.PkgInfo
|
|
replaced *removeTransactionState
|
|
installedPaths []string
|
|
newBackups []pathBackup
|
|
txDir string
|
|
}
|
|
|
|
type removeTransactionState struct {
|
|
pkgName string
|
|
info *types.PkgInfo
|
|
backups []pathBackup
|
|
}
|
|
|
|
type pathBackup struct {
|
|
originalPath string
|
|
backupPath string
|
|
}
|