Улучшения обработки зависимостей и фильтрации установленных пакетов

- Добавлена поддержка версионных ограничений при установке пакетов
- Улучшена логика фильтрации уже установленных пакетов
- Добавлен метод GetInstalledVersion для всех менеджеров пакетов
- Активированы тесты для систем archlinux, alpine, opensuse-leap
- Улучшена обработка переменных в скриптах

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-01-16 01:01:01 +03:00
parent b649a459b8
commit 3d9f4a0985
25 changed files with 1330 additions and 45 deletions

137
pkg/depver/depver.go Normal file
View File

@@ -0,0 +1,137 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package depver provides parsing and comparison of versioned dependencies
// in PKGBUILD-style format (e.g., "gcc>=5.0", "openssl>=1.1.0").
package depver
import (
"strings"
"gitea.plemya-x.ru/xpamych/vercmp"
)
// Operator represents a version comparison operator.
type Operator string
const (
OpNone Operator = "" // No version constraint
OpEq Operator = "=" // Equal to
OpGt Operator = ">" // Greater than
OpGe Operator = ">=" // Greater than or equal to
OpLt Operator = "<" // Less than
OpLe Operator = "<=" // Less than or equal to
)
// Dependency represents a package dependency with optional version constraint.
type Dependency struct {
Name string // Package name (e.g., "gcc")
Operator Operator // Comparison operator (e.g., OpGe for ">=")
Version string // Version string (e.g., "5.0")
}
// operators lists all supported operators in order of decreasing length
// (to ensure ">=" is matched before ">").
var operators = []Operator{OpGe, OpLe, OpGt, OpLt, OpEq}
// Parse parses a dependency string in PKGBUILD format.
// Examples:
// - "gcc>=5.0" -> Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"}
// - "openssl" -> Dependency{Name: "openssl", Operator: OpNone, Version: ""}
// - "cmake>=3.10" -> Dependency{Name: "cmake", Operator: OpGe, Version: "3.10"}
func Parse(dep string) Dependency {
dep = strings.TrimSpace(dep)
if dep == "" {
return Dependency{}
}
// Try each operator (longer ones first)
for _, op := range operators {
if idx := strings.Index(dep, string(op)); idx > 0 {
return Dependency{
Name: strings.TrimSpace(dep[:idx]),
Operator: op,
Version: strings.TrimSpace(dep[idx+len(op):]),
}
}
}
// No operator found - just a package name
return Dependency{
Name: dep,
Operator: OpNone,
Version: "",
}
}
// ParseMultiple parses multiple dependency strings.
func ParseMultiple(deps []string) []Dependency {
result := make([]Dependency, 0, len(deps))
for _, dep := range deps {
if dep != "" {
result = append(result, Parse(dep))
}
}
return result
}
// String returns the dependency in PKGBUILD format.
func (d Dependency) String() string {
if d.Operator == OpNone || d.Version == "" {
return d.Name
}
return d.Name + string(d.Operator) + d.Version
}
// Satisfies checks if the given version satisfies the dependency constraint.
// Returns true if:
// - The dependency has no version constraint (OpNone)
// - The installed version satisfies the operator/version requirement
func (d Dependency) Satisfies(installedVersion string) bool {
if d.Operator == OpNone || d.Version == "" {
return true
}
if installedVersion == "" {
return false
}
// vercmp.Compare returns:
// -1 if installedVersion < d.Version
// 0 if installedVersion == d.Version
// 1 if installedVersion > d.Version
cmp := vercmp.Compare(installedVersion, d.Version)
switch d.Operator {
case OpEq:
return cmp == 0
case OpGt:
return cmp > 0
case OpGe:
return cmp >= 0
case OpLt:
return cmp < 0
case OpLe:
return cmp <= 0
default:
return true
}
}
// HasVersionConstraint returns true if the dependency has a version constraint.
func (d Dependency) HasVersionConstraint() bool {
return d.Operator != OpNone && d.Version != ""
}

347
pkg/depver/depver_test.go Normal file
View File

@@ -0,0 +1,347 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package depver
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
expected Dependency
}{
{
name: "simple package name",
input: "gcc",
expected: Dependency{
Name: "gcc",
Operator: OpNone,
Version: "",
},
},
{
name: "greater or equal",
input: "gcc>=5.0",
expected: Dependency{
Name: "gcc",
Operator: OpGe,
Version: "5.0",
},
},
{
name: "less or equal",
input: "openssl<=1.1.0",
expected: Dependency{
Name: "openssl",
Operator: OpLe,
Version: "1.1.0",
},
},
{
name: "greater than",
input: "cmake>3.10",
expected: Dependency{
Name: "cmake",
Operator: OpGt,
Version: "3.10",
},
},
{
name: "less than",
input: "python<4.0",
expected: Dependency{
Name: "python",
Operator: OpLt,
Version: "4.0",
},
},
{
name: "equal",
input: "nodejs=18.0.0",
expected: Dependency{
Name: "nodejs",
Operator: OpEq,
Version: "18.0.0",
},
},
{
name: "with spaces around",
input: " gcc>=5.0 ",
expected: Dependency{
Name: "gcc",
Operator: OpGe,
Version: "5.0",
},
},
{
name: "complex version",
input: "glibc>=2.17-326",
expected: Dependency{
Name: "glibc",
Operator: OpGe,
Version: "2.17-326",
},
},
{
name: "empty string",
input: "",
expected: Dependency{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Parse(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseMultiple(t *testing.T) {
input := []string{"gcc>=5.0", "openssl", "cmake>=3.10", ""}
expected := []Dependency{
{Name: "gcc", Operator: OpGe, Version: "5.0"},
{Name: "openssl", Operator: OpNone, Version: ""},
{Name: "cmake", Operator: OpGe, Version: "3.10"},
}
result := ParseMultiple(input)
assert.Equal(t, expected, result)
}
func TestDependency_String(t *testing.T) {
tests := []struct {
name string
dep Dependency
expected string
}{
{
name: "no version",
dep: Dependency{Name: "gcc", Operator: OpNone, Version: ""},
expected: "gcc",
},
{
name: "with version",
dep: Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"},
expected: "gcc>=5.0",
},
{
name: "equal operator",
dep: Dependency{Name: "python", Operator: OpEq, Version: "3.11"},
expected: "python=3.11",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.dep.String()
assert.Equal(t, tt.expected, result)
})
}
}
func TestDependency_Satisfies(t *testing.T) {
tests := []struct {
name string
dep Dependency
installedVersion string
expected bool
}{
{
name: "no constraint - always satisfied",
dep: Dependency{Name: "gcc", Operator: OpNone, Version: ""},
installedVersion: "5.0",
expected: true,
},
{
name: "ge - satisfied",
dep: Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"},
installedVersion: "5.0",
expected: true,
},
{
name: "ge - greater version satisfied",
dep: Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"},
installedVersion: "6.0",
expected: true,
},
{
name: "ge - not satisfied",
dep: Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"},
installedVersion: "4.9",
expected: false,
},
{
name: "gt - satisfied",
dep: Dependency{Name: "gcc", Operator: OpGt, Version: "5.0"},
installedVersion: "5.1",
expected: true,
},
{
name: "gt - equal not satisfied",
dep: Dependency{Name: "gcc", Operator: OpGt, Version: "5.0"},
installedVersion: "5.0",
expected: false,
},
{
name: "le - satisfied",
dep: Dependency{Name: "gcc", Operator: OpLe, Version: "5.0"},
installedVersion: "5.0",
expected: true,
},
{
name: "le - lesser satisfied",
dep: Dependency{Name: "gcc", Operator: OpLe, Version: "5.0"},
installedVersion: "4.9",
expected: true,
},
{
name: "le - not satisfied",
dep: Dependency{Name: "gcc", Operator: OpLe, Version: "5.0"},
installedVersion: "5.1",
expected: false,
},
{
name: "lt - satisfied",
dep: Dependency{Name: "gcc", Operator: OpLt, Version: "5.0"},
installedVersion: "4.9",
expected: true,
},
{
name: "lt - equal not satisfied",
dep: Dependency{Name: "gcc", Operator: OpLt, Version: "5.0"},
installedVersion: "5.0",
expected: false,
},
{
name: "eq - satisfied",
dep: Dependency{Name: "gcc", Operator: OpEq, Version: "5.0"},
installedVersion: "5.0",
expected: true,
},
{
name: "eq - not satisfied",
dep: Dependency{Name: "gcc", Operator: OpEq, Version: "5.0"},
installedVersion: "5.1",
expected: false,
},
{
name: "empty installed version",
dep: Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"},
installedVersion: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.dep.Satisfies(tt.installedVersion)
assert.Equal(t, tt.expected, result)
})
}
}
func TestDependency_ForManager(t *testing.T) {
dep := Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"}
tests := []struct {
manager string
expected string
}{
{"pacman", "gcc>=5.0"},
{"apt", "gcc"},
{"dnf", "gcc >= 5.0"},
{"yum", "gcc >= 5.0"},
{"apk", "gcc>=5.0"},
{"zypper", "gcc >= 5.0"},
{"apt-rpm", "gcc >= 5.0"},
{"unknown", "gcc>=5.0"},
}
for _, tt := range tests {
t.Run(tt.manager, func(t *testing.T) {
result := dep.ForManager(tt.manager)
assert.Equal(t, tt.expected, result)
})
}
// Test without version constraint
depNoVersion := Dependency{Name: "gcc", Operator: OpNone, Version: ""}
for _, tt := range tests {
t.Run(tt.manager+"_no_version", func(t *testing.T) {
result := depNoVersion.ForManager(tt.manager)
assert.Equal(t, "gcc", result)
})
}
}
func TestDependency_ForNfpm(t *testing.T) {
dep := Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"}
tests := []struct {
format string
expected string
}{
{"deb", "gcc (>= 5.0)"},
{"rpm", "gcc >= 5.0"},
{"apk", "gcc>=5.0"},
{"archlinux", "gcc>=5.0"},
{"unknown", "gcc>=5.0"},
}
for _, tt := range tests {
t.Run(tt.format, func(t *testing.T) {
result := dep.ForNfpm(tt.format)
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasVersionConstraint(t *testing.T) {
tests := []struct {
name string
dep Dependency
expected bool
}{
{
name: "has constraint",
dep: Dependency{Name: "gcc", Operator: OpGe, Version: "5.0"},
expected: true,
},
{
name: "no operator",
dep: Dependency{Name: "gcc", Operator: OpNone, Version: ""},
expected: false,
},
{
name: "operator but no version",
dep: Dependency{Name: "gcc", Operator: OpGe, Version: ""},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.dep.HasVersionConstraint()
assert.Equal(t, tt.expected, result)
})
}
}

132
pkg/depver/format.go Normal file
View File

@@ -0,0 +1,132 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package depver
import "fmt"
// ForManager formats the dependency for a specific package manager.
// Different package managers have different syntax for version constraints:
//
// pacman (Arch): "gcc>=5.0" (no changes)
// apt (Debian): "gcc" (version ignored for install command)
// dnf/yum (Fedora): "gcc >= 5.0" (with spaces)
// apk (Alpine): "gcc>=5.0" (no changes)
// zypper (openSUSE): "gcc >= 5.0" (with spaces)
// apt-rpm (ALT): "gcc >= 5.0" (with spaces)
func (d Dependency) ForManager(managerName string) string {
if d.Name == "" {
return ""
}
// No version constraint - just return the name
if d.Operator == OpNone || d.Version == "" {
return d.Name
}
switch managerName {
case "apt":
// APT doesn't support version constraints in 'apt install' command
// Versions are checked after installation
return d.Name
case "pacman":
// Pacman uses PKGBUILD-style: package>=version (no spaces)
return fmt.Sprintf("%s%s%s", d.Name, d.Operator, d.Version)
case "apk":
// Alpine APK uses similar syntax to pacman
return fmt.Sprintf("%s%s%s", d.Name, d.Operator, d.Version)
case "dnf", "yum":
// DNF/YUM use RPM-style: "package >= version" (with spaces)
return fmt.Sprintf("%s %s %s", d.Name, d.Operator, d.Version)
case "zypper":
// Zypper uses RPM-style with spaces
return fmt.Sprintf("%s %s %s", d.Name, d.Operator, d.Version)
case "apt-rpm":
// ALT Linux apt-rpm uses RPM-style
return fmt.Sprintf("%s %s %s", d.Name, d.Operator, d.Version)
default:
// Default: PKGBUILD-style (no spaces)
return fmt.Sprintf("%s%s%s", d.Name, d.Operator, d.Version)
}
}
// ForManagerMultiple formats multiple dependencies for a specific package manager.
func ForManagerMultiple(deps []Dependency, managerName string) []string {
result := make([]string, 0, len(deps))
for _, dep := range deps {
if formatted := dep.ForManager(managerName); formatted != "" {
result = append(result, formatted)
}
}
return result
}
// ForNfpm formats the dependency for nfpm package building.
// Different package formats have different dependency syntax:
//
// deb: "package (>= version)"
// rpm: "package >= version"
// apk: "package>=version"
// archlinux: "package>=version"
func (d Dependency) ForNfpm(pkgFormat string) string {
if d.Name == "" {
return ""
}
// No version constraint - just return the name
if d.Operator == OpNone || d.Version == "" {
return d.Name
}
switch pkgFormat {
case "deb":
// Debian uses: package (>= version)
return fmt.Sprintf("%s (%s %s)", d.Name, d.Operator, d.Version)
case "rpm":
// RPM uses: package >= version
return fmt.Sprintf("%s %s %s", d.Name, d.Operator, d.Version)
case "apk":
// Alpine uses: package>=version
return fmt.Sprintf("%s%s%s", d.Name, d.Operator, d.Version)
case "archlinux":
// Arch uses: package>=version
return fmt.Sprintf("%s%s%s", d.Name, d.Operator, d.Version)
default:
// Default: no spaces
return fmt.Sprintf("%s%s%s", d.Name, d.Operator, d.Version)
}
}
// ForNfpmMultiple formats multiple dependencies for nfpm.
func ForNfpmMultiple(deps []Dependency, pkgFormat string) []string {
result := make([]string, 0, len(deps))
for _, dep := range deps {
if formatted := dep.ForNfpm(pkgFormat); formatted != "" {
result = append(result, formatted)
}
}
return result
}