9 Commits

Author SHA1 Message Date
b649a459b8 Merge pull request 'Исправлен ключ дефолтного репозитория в конфигурации' (#139) from fix/default-repo-config-key into master
All checks were successful
Pre-commit / pre-commit (push) Successful in 4m3s
Create Release / changelog (push) Successful in 2m38s
Reviewed-on: #139
2025-12-15 20:28:51 +00:00
e8c20bad25 Исправлен ключ дефолтного репозитория в конфигурации
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 3m51s
Ключ "repos" изменён на "repo" для соответствия тегу koanf в структуре Config.
Это исправляет проблему, когда дефолтный репозиторий alr-default не загружался.
2025-12-15 23:22:15 +03:00
df69f3dcab Merge pull request 'Исправлена повторная сборка подпакетов мультипакета' (#138) from fix/multipackage-dependencies into master
All checks were successful
Pre-commit / pre-commit (push) Successful in 4m8s
Create Release / changelog (push) Successful in 2m43s
Reviewed-on: #138
2025-12-15 19:42:57 +00:00
a44da806d4 Исправлена повторная сборка подпакетов мультипакета
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 4m37s
При установке пакета с зависимостями на другие подпакеты того же
мультипакета теперь каждый подпакет собирается только один раз.
2025-12-15 22:36:49 +03:00
6567f8a71f Merge pull request 'Извлечение имени репозитория из пути к скрипту при сборке через -s' (#137) from fix/repository-name-from-path into master
All checks were successful
Pre-commit / pre-commit (push) Successful in 3m42s
Create Release / changelog (push) Successful in 2m26s
Reviewed-on: #137
2025-12-09 08:38:45 +00:00
7448d91817 Извлечение имени репозитория из пути к скрипту при сборке через -s
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 3m47s
2025-12-09 11:33:14 +03:00
f775641cb7 Обновлён сгенерированный package_gen.go с лицензией
All checks were successful
Pre-commit / pre-commit (push) Successful in 3m34s
2025-12-08 22:26:00 +00:00
e05396f214 Добавлен лицензионный заголовок в генератор package_gen.go
Some checks failed
Pre-commit / pre-commit (push) Failing after 3m12s
2025-12-08 22:19:07 +00:00
5e094fa69f gitignore
Some checks failed
Pre-commit / pre-commit (push) Failing after 3m16s
2025-12-08 21:59:20 +00:00
8 changed files with 199 additions and 78 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ commit_msg.txt
/scripts/.claude/settings.local.json /scripts/.claude/settings.local.json
/ALR /ALR
.claude/settings.local.json .claude/settings.local.json
.directory

View File

@@ -202,6 +202,23 @@ func main() {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`// 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/>.
`)
buf.WriteString("// DO NOT EDIT MANUALLY. This file is generated.\n") buf.WriteString("// DO NOT EDIT MANUALLY. This file is generated.\n")
buf.WriteString("package alrsh") buf.WriteString("package alrsh")

View File

@@ -39,12 +39,13 @@ import (
) )
type BuildInput struct { type BuildInput struct {
opts *types.BuildOpts opts *types.BuildOpts
info *distro.OSRelease info *distro.OSRelease
pkgFormat string pkgFormat string
script string script string
repository string repository string
packages []string packages []string
skipDepsBuilding bool // Пропустить сборку зависимостей (используется при вызове из BuildALRDeps)
} }
func (bi *BuildInput) GobEncode() ([]byte, error) { func (bi *BuildInput) GobEncode() ([]byte, error) {
@@ -242,9 +243,10 @@ type Builder struct {
} }
type BuildArgs struct { type BuildArgs struct {
Opts *types.BuildOpts Opts *types.BuildOpts
Info *distro.OSRelease Info *distro.OSRelease
PkgFormat_ string PkgFormat_ string
SkipDepsBuilding bool // Пропустить сборку зависимостей (используется при вызове из BuildALRDeps)
} }
func (b *BuildArgs) BuildOpts() *types.BuildOpts { func (b *BuildArgs) BuildOpts() *types.BuildOpts {
@@ -278,12 +280,13 @@ func (b *Builder) BuildPackageFromDb(
scriptInfo := b.scriptResolver.ResolveScript(ctx, args.Package) scriptInfo := b.scriptResolver.ResolveScript(ctx, args.Package)
return b.BuildPackage(ctx, &BuildInput{ return b.BuildPackage(ctx, &BuildInput{
script: scriptInfo.Script, script: scriptInfo.Script,
repository: scriptInfo.Repository, repository: scriptInfo.Repository,
packages: args.Packages, packages: args.Packages,
pkgFormat: args.PkgFormat(), pkgFormat: args.PkgFormat(),
opts: args.Opts, opts: args.Opts,
info: args.Info, info: args.Info,
skipDepsBuilding: args.SkipDepsBuilding,
}) })
} }
@@ -293,7 +296,7 @@ func (b *Builder) BuildPackageFromScript(
) ([]*BuiltDep, error) { ) ([]*BuiltDep, error) {
return b.BuildPackage(ctx, &BuildInput{ return b.BuildPackage(ctx, &BuildInput{
script: args.Script, script: args.Script,
repository: "default", repository: ExtractRepoNameFromPath(args.Script),
packages: args.Packages, packages: args.Packages,
pkgFormat: args.PkgFormat(), pkgFormat: args.PkgFormat(),
opts: args.Opts, opts: args.Opts,
@@ -407,13 +410,14 @@ func (b *Builder) BuildPackage(
// We filter so as not to re-build what has already been built at the `installBuildDeps` stage. // We filter so as not to re-build what has already been built at the `installBuildDeps` stage.
var filteredDepends []string var filteredDepends []string
// Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей // Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей
// Используем имена из varsOfPackages, так как input.packages может быть пустым
currentPackageNames := make(map[string]struct{}) currentPackageNames := make(map[string]struct{})
for _, pkg := range input.packages { for _, vars := range varsOfPackages {
currentPackageNames[pkg] = struct{}{} currentPackageNames[vars.Name] = struct{}{}
} }
for _, d := range depends { for _, d := range depends {
if _, found := depNames[d]; !found { if _, found := depNames[d]; !found {
// Исключаем зависимости, которые являются подпакетами текущего мультипакета // Исключаем зависимости, которые являются подпакетами текущего мультипакета
@@ -423,10 +427,16 @@ func (b *Builder) BuildPackage(
} }
} }
slog.Debug("BuildALRDeps") var newBuiltDeps []*BuiltDep
newBuiltDeps, repoDeps, err := b.BuildALRDeps(ctx, input, filteredDepends) var repoDeps []string
if err != nil {
return nil, err // Пропускаем сборку зависимостей если флаг установлен (вызов из BuildALRDeps)
if !input.skipDepsBuilding {
slog.Debug("BuildALRDeps")
newBuiltDeps, repoDeps, err = b.BuildALRDeps(ctx, input, filteredDepends)
if err != nil {
return nil, err
}
} }
slog.Debug("PrepareDirs") slog.Debug("PrepareDirs")
@@ -580,65 +590,68 @@ func (b *Builder) BuildALRDeps(
// Системные зависимости возвращаем как repoDeps // Системные зависимости возвращаем как repoDeps
repoDeps = systemDeps repoDeps = systemDeps
// Шаг 2: Собираем список всех пакетов из дерева для топологической сортировки // Шаг 2: Топологическая сортировка (от корней к листьям)
allFound := make(map[string][]alrsh.Package) sortedPkgs, err := TopologicalSort(depTree)
for baseName, node := range depTree {
allFound[baseName] = []alrsh.Package{*node.Package}
}
// Шаг 3: Топологическая сортировка (от корней к листьям)
sortedPkgs, err := TopologicalSort(depTree, allFound)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to sort dependencies: %w", err) return nil, nil, fmt.Errorf("failed to sort dependencies: %w", err)
} }
// Шаг 3: Группируем подпакеты по basePkgName для оптимизации сборки
// Если несколько подпакетов из одного мультипакета, собираем их вместе
builtBasePkgs := make(map[string]bool) // Уже собранные базовые пакеты
// Шаг 4: Собираем пакеты в правильном порядке, проверяя кеш // Шаг 4: Собираем пакеты в правильном порядке, проверяя кеш
for _, basePkgName := range sortedPkgs { for _, pkgName := range sortedPkgs {
node := depTree[basePkgName] node := depTree[pkgName]
if node == nil { if node == nil {
continue continue
} }
pkg := node.Package pkg := node.Package
basePkgName := node.BasePkgName
// Находим ВСЕ подпакеты с этим BasePkgName // Если базовый пакет уже собран, пропускаем
allSubpkgs, err := b.findAllSubpackages(ctx, basePkgName, pkg.Repository) if builtBasePkgs[basePkgName] {
if err != nil { continue
return nil, nil, fmt.Errorf("failed to find subpackages for %s: %w", basePkgName, err)
} }
// Проверяем кеш для ВСЕХ подпакетов // Собираем только запрошенный подпакет (или все, если запрошен basePkgName)
packagesToBuilt := []string{pkgName}
// Проверяем кеш для запрошенного подпакета
scriptInfo := b.scriptResolver.ResolveScript(ctx, pkg) scriptInfo := b.scriptResolver.ResolveScript(ctx, pkg)
buildInput := &BuildInput{ buildInput := &BuildInput{
script: scriptInfo.Script, script: scriptInfo.Script,
repository: scriptInfo.Repository, repository: scriptInfo.Repository,
packages: allSubpkgs, packages: packagesToBuilt,
pkgFormat: input.PkgFormat(), pkgFormat: input.PkgFormat(),
opts: input.BuildOpts(), opts: input.BuildOpts(),
info: input.OSRelease(), info: input.OSRelease(),
} }
cachedDeps, allInCache, err := b.checkCacheForAllSubpackages(ctx, buildInput, basePkgName, allSubpkgs) cachedDeps, allInCache, err := b.checkCacheForAllSubpackages(ctx, buildInput, basePkgName, packagesToBuilt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if allInCache { if allInCache {
// Все подпакеты в кеше, используем их // Подпакет в кеше, используем его
buildDeps = append(buildDeps, cachedDeps...) buildDeps = append(buildDeps, cachedDeps...)
continue continue
} }
// Собираем пакет (без рекурсивной сборки зависимостей, так как они уже собраны) // Собираем только запрошенный подпакет
// SkipDepsBuilding: true предотвращает рекурсивный вызов BuildALRDeps
res, err := b.BuildPackageFromDb( res, err := b.BuildPackageFromDb(
ctx, ctx,
&BuildPackageFromDbArgs{ &BuildPackageFromDbArgs{
Package: pkg, Package: pkg,
Packages: allSubpkgs, Packages: packagesToBuilt,
BuildArgs: BuildArgs{ BuildArgs: BuildArgs{
Opts: input.BuildOpts(), Opts: input.BuildOpts(),
Info: input.OSRelease(), Info: input.OSRelease(),
PkgFormat_: input.PkgFormat(), PkgFormat_: input.PkgFormat(),
SkipDepsBuilding: true,
}, },
}, },
) )
@@ -647,6 +660,7 @@ func (b *Builder) BuildALRDeps(
} }
buildDeps = append(buildDeps, res...) buildDeps = append(buildDeps, res...)
builtBasePkgs[basePkgName] = true
} }
buildDeps = removeDuplicates(buildDeps) buildDeps = removeDuplicates(buildDeps)

View File

@@ -27,6 +27,7 @@ import (
type DependencyNode struct { type DependencyNode struct {
Package *alrsh.Package Package *alrsh.Package
BasePkgName string BasePkgName string
PkgName string // Имя конкретного подпакета (может отличаться от BasePkgName)
Dependencies []string // Имена зависимостей Dependencies []string // Имена зависимостей
} }
@@ -82,8 +83,9 @@ func (b *Builder) ResolveDependencyTree(
baseName = pkg.Name baseName = pkg.Name
} }
// Если уже обработали этот базовый пакет, пропускаем // Используем имя конкретного подпакета как ключ (не basePkgName)
if resolved[baseName] != nil { // Это позволяет собирать только запрошенный подпакет, а не весь мультипакет
if resolved[pkgName] != nil {
continue continue
} }
@@ -96,10 +98,11 @@ func (b *Builder) ResolveDependencyTree(
allDeps := append([]string{}, deps...) allDeps := append([]string{}, deps...)
allDeps = append(allDeps, buildDeps...) allDeps = append(allDeps, buildDeps...)
// Добавляем узел в resolved // Добавляем узел в resolved с ключом = имя подпакета
resolved[baseName] = &DependencyNode{ resolved[pkgName] = &DependencyNode{
Package: &pkg, Package: &pkg,
BasePkgName: baseName, BasePkgName: baseName,
PkgName: pkgName,
Dependencies: allDeps, Dependencies: allDeps,
} }
@@ -129,8 +132,8 @@ func (b *Builder) ResolveDependencyTree(
} }
// TopologicalSort выполняет топологическую сортировку пакетов по зависимостям // TopologicalSort выполняет топологическую сортировку пакетов по зависимостям
// Возвращает список базовых имен пакетов в порядке сборки (от корней к листьям) // Возвращает список имен подпакетов в порядке сборки (от корней к листьям)
func TopologicalSort(nodes map[string]*DependencyNode, allPkgs map[string][]alrsh.Package) ([]string, error) { func TopologicalSort(nodes map[string]*DependencyNode) ([]string, error) {
// Список для результата // Список для результата
var result []string var result []string
@@ -140,51 +143,42 @@ func TopologicalSort(nodes map[string]*DependencyNode, allPkgs map[string][]alrs
// Множество узлов в текущем пути (для обнаружения циклов) // Множество узлов в текущем пути (для обнаружения циклов)
inStack := make(map[string]bool) inStack := make(map[string]bool)
var visit func(basePkgName string) error var visit func(pkgName string) error
visit = func(basePkgName string) error { visit = func(pkgName string) error {
if visited[basePkgName] { if visited[pkgName] {
return nil return nil
} }
if inStack[basePkgName] { if inStack[pkgName] {
return fmt.Errorf("circular dependency detected: %s", basePkgName) return fmt.Errorf("circular dependency detected: %s", pkgName)
} }
node := nodes[basePkgName] node := nodes[pkgName]
if node == nil { if node == nil {
// Это системный пакет, игнорируем // Это системный пакет или пакет не в дереве, игнорируем
return nil return nil
} }
inStack[basePkgName] = true inStack[pkgName] = true
// Посещаем все зависимости // Посещаем все зависимости
for _, dep := range node.Dependencies { for _, dep := range node.Dependencies {
// Находим базовое имя для зависимости // Используем имя зависимости напрямую (это имя подпакета)
depBaseName := dep if err := visit(dep); err != nil {
// Проверяем, есть ли этот пакет в allPkgs
if pkgs, ok := allPkgs[dep]; ok && len(pkgs) > 0 {
if pkgs[0].BasePkgName != "" {
depBaseName = pkgs[0].BasePkgName
}
}
if err := visit(depBaseName); err != nil {
return err return err
} }
} }
inStack[basePkgName] = false inStack[pkgName] = false
visited[basePkgName] = true visited[pkgName] = true
result = append(result, basePkgName) result = append(result, pkgName)
return nil return nil
} }
// Посещаем все узлы // Посещаем все узлы
for basePkgName := range nodes { for pkgName := range nodes {
if err := visit(basePkgName); err != nil { if err := visit(pkgName); err != nil {
return nil, err return nil, err
} }
} }

View File

@@ -331,3 +331,29 @@ func removeDuplicatesSources(sources, checksums []string) ([]string, []string) {
} }
return newSources, newChecksums return newSources, newChecksums
} }
// ExtractRepoNameFromPath извлекает имя репозитория из пути к скрипту.
// Ожидаемый формат: repo-name/package-name/alr.sh или /abs/path/repo-name/package-name/alr.sh
// Возвращает "default", если не удалось извлечь имя.
func ExtractRepoNameFromPath(scriptPath string) string {
// Нормализуем путь
cleanPath := filepath.Clean(scriptPath)
// Разбиваем путь на компоненты
dir := filepath.Dir(cleanPath) // package-name
if dir == "." || dir == "/" {
return "default"
}
repoDir := filepath.Dir(dir) // repo-name
if repoDir == "." || repoDir == "/" {
return "default"
}
repoName := filepath.Base(repoDir)
if repoName == "." || repoName == "/" || repoName == "" {
return "default"
}
return repoName
}

View File

@@ -155,4 +155,57 @@ func TestRegexpALRPackageName(t *testing.T) {
} }
}) })
} }
}
func TestExtractRepoNameFromPath(t *testing.T) {
tests := []struct {
name string
scriptPath string
expectedRepo string
}{
{
name: "относительный путь - стандартная структура",
scriptPath: "alr-default/alr-bin/alr.sh",
expectedRepo: "alr-default",
},
{
name: "абсолютный путь",
scriptPath: "/home/user/repos/alr-default/alr-bin/alr.sh",
expectedRepo: "alr-default",
},
{
name: "репозиторий без префикса alr-",
scriptPath: "my-repo/my-package/alr.sh",
expectedRepo: "my-repo",
},
{
name: "только имя файла",
scriptPath: "alr.sh",
expectedRepo: "default",
},
{
name: "один уровень директории",
scriptPath: "package/alr.sh",
expectedRepo: "default",
},
{
name: "путь с точками",
scriptPath: "./alr-default/alr-bin/alr.sh",
expectedRepo: "alr-default",
},
{
name: "путь с двойными точками",
scriptPath: "../alr-default/alr-bin/alr.sh",
expectedRepo: "alr-default",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractRepoNameFromPath(tt.scriptPath)
if result != tt.expectedRepo {
t.Errorf("ExtractRepoNameFromPath(%q) = %q, ожидается %q", tt.scriptPath, result, tt.expectedRepo)
}
})
}
} }

View File

@@ -59,7 +59,7 @@ func defaultConfigKoanf() *koanf.Koanf {
"logLevel": "info", "logLevel": "info",
"autoPull": true, "autoPull": true,
"updateSystemOnUpgrade": false, "updateSystemOnUpgrade": false,
"repos": []types.Repo{ "repo": []types.Repo{
{ {
Name: "alr-default", Name: "alr-default",
URL: "https://gitea.plemya-x.ru/Plemya-x/alr-default.git", URL: "https://gitea.plemya-x.ru/Plemya-x/alr-default.git",

View File

@@ -1,3 +1,19 @@
// 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/>.
// DO NOT EDIT MANUALLY. This file is generated. // DO NOT EDIT MANUALLY. This file is generated.
package alrsh package alrsh