From 8978cc28550a914d780a3df1873fb6af4d4c6946 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Mon, 3 Feb 2025 19:15:54 +0300 Subject: [PATCH] wip: add split packages support --- build.go | 60 +- coverage-badge.svg | 4 +- internal/config/config.go | 7 + internal/db/db.go | 6 +- internal/dl/file.go | 1 + internal/shutils/decoder/decoder.go | 21 + internal/translations/default.pot | 122 ++-- internal/translations/po/ru/default.po | 101 ++-- internal/types/build.go | 50 ++ pkg/build/build.go | 760 +++++++------------------ pkg/build/build_legacy.go | 479 ++++++++++++++++ pkg/build/install.go | 45 +- pkg/repos/pull.go | 152 +++-- pkg/repos/pull_internal_test.go | 173 ++++++ pkg/repos/repos_legacy.go | 3 +- pkg/repos/utils.go | 41 ++ 16 files changed, 1282 insertions(+), 743 deletions(-) create mode 100644 pkg/build/build_legacy.go create mode 100644 pkg/repos/pull_internal_test.go diff --git a/build.go b/build.go index d259986..2c63799 100644 --- a/build.go +++ b/build.go @@ -28,9 +28,11 @@ import ( "github.com/urfave/cli/v2" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" + database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" "gitea.plemya-x.ru/Plemya-x/ALR/internal/osutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/build" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos" ) @@ -59,30 +61,43 @@ func BuildCmd() *cli.Command { }, Action: func(c *cli.Context) error { ctx := c.Context + cfg := config.New() + db := database.New(cfg) + rs := repos.New(cfg, db) + err := db.Init(ctx) + if err != nil { + slog.Error(gotext.Get("Error db init"), "err", err) + os.Exit(1) + } - var script string + var script, packageName string // Проверяем, установлен ли флаг script (-s) + repoDir := cfg.GetPaths(ctx).RepoDir + switch { case c.IsSet("script"): script = c.String("script") case c.IsSet("package"): + // TODO: refactor packageInput := c.String("package") - if filepath.Dir(packageInput) == "." { - // Не указана директория репозитория, используем 'default' как префикс - script = filepath.Join(config.GetPaths(ctx).RepoDir, "default", packageInput, "alr.sh") + pkgs, _, _ := rs.FindPkgs(ctx, []string{packageInput}) + pkg := pkgs[packageInput] + if pkg[0].BasePkgName != "" { + script = filepath.Join(repoDir, pkg[0].Repository, pkg[0].BasePkgName, "alr.sh") + packageName = pkg[0].Name } else { - // Используем путь с указанным репозиторием - script = filepath.Join(config.GetPaths(ctx).RepoDir, packageInput, "alr.sh") + script = filepath.Join(repoDir, pkg[0].Repository, pkg[0].Name, "alr.sh") } + default: - script = filepath.Join(config.GetPaths(ctx).RepoDir, "alr.sh") + script = filepath.Join(repoDir, "alr.sh") } // Проверка автоматического пулла репозиториев - if config.GetInstance(ctx).AutoPull(ctx) { - err := repos.Pull(ctx, config.Config(ctx).Repos) + if cfg.AutoPull(ctx) { + err := rs.Pull(ctx, cfg.Repos(ctx)) if err != nil { slog.Error(gotext.Get("Error pulling repositories"), "err", err) os.Exit(1) @@ -96,13 +111,28 @@ func BuildCmd() *cli.Command { os.Exit(1) } + info, err := distro.ParseOSRelease(ctx) + if err != nil { + slog.Error(gotext.Get("Error parsing os release"), "err", err) + os.Exit(1) + } + + builder := build.New( + ctx, + types.BuildOpts{ + Package: packageName, + Script: script, + Manager: mgr, + Clean: c.Bool("clean"), + Interactive: c.Bool("interactive"), + }, + rs, + info, + cfg, + ) + // Сборка пакета - pkgPaths, _, err := build.BuildPackage(ctx, types.BuildOpts{ - Script: script, - Manager: mgr, - Clean: c.Bool("clean"), - Interactive: c.Bool("interactive"), - }) + pkgPaths, _, err := builder.BuildPackage(ctx) if err != nil { slog.Error(gotext.Get("Error building package"), "err", err) os.Exit(1) diff --git a/coverage-badge.svg b/coverage-badge.svg index 2a6facc..b7b7768 100644 --- a/coverage-badge.svg +++ b/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 19.2% - 19.2% + 19.7% + 19.7% diff --git a/internal/config/config.go b/internal/config/config.go index f8457c6..62df450 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -170,3 +170,10 @@ func (c *ALRConfig) AutoPull(ctx context.Context) bool { }) return c.cfg.AutoPull } + +func (c *ALRConfig) PagerStyle(ctx context.Context) string { + c.cfgOnce.Do(func() { + c.Load(ctx) + }) + return c.cfg.PagerStyle +} diff --git a/internal/db/db.go b/internal/db/db.go index 7a8ee6d..b76b748 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -31,10 +31,11 @@ import ( // CurrentVersion is the current version of the database. // The database is reset if its version doesn't match this. -const CurrentVersion = 2 +const CurrentVersion = 3 // Package is a ALR package's database representation type Package struct { + BasePkgName string `sh:"base" db:"basepkg_name"` Name string `sh:"name,required" db:"name"` Version string `sh:"version,required" db:"version"` Release int `sh:"release,required" db:"release"` @@ -99,6 +100,7 @@ func (d *Database) initDB(ctx context.Context) error { conn := d.conn _, err := conn.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS pkgs ( + basepkg_name TEXT NOT NULL, name TEXT NOT NULL, repository TEXT NOT NULL, version TEXT NOT NULL, @@ -196,6 +198,7 @@ func (d *Database) IsEmpty(ctx context.Context) bool { func (d *Database) InsertPackage(ctx context.Context, pkg Package) error { _, err := d.conn.NamedExecContext(ctx, ` INSERT OR REPLACE INTO pkgs ( + basepkg_name, name, repository, version, @@ -213,6 +216,7 @@ func (d *Database) InsertPackage(ctx context.Context, pkg Package) error { builddepends, optdepends ) VALUES ( + :basepkg_name, :name, :repository, :version, diff --git a/internal/dl/file.go b/internal/dl/file.go index 2e63b39..96ce294 100644 --- a/internal/dl/file.go +++ b/internal/dl/file.go @@ -123,6 +123,7 @@ func (FileDownloader) Download(ctx context.Context, opts Options) (Type, string, } else { out = fl } + defer out.Close() h, err := opts.NewHash() if err != nil { diff --git a/internal/shutils/decoder/decoder.go b/internal/shutils/decoder/decoder.go index 629e2a7..d59e8ea 100644 --- a/internal/shutils/decoder/decoder.go +++ b/internal/shutils/decoder/decoder.go @@ -197,6 +197,27 @@ func (d *Decoder) GetFuncP(name string, prepare PrepareFunc) (ScriptFunc, bool) }, true } +// TODO: replace +func (d *Decoder) GetFuncSub(name string) ( + func(ctx context.Context, opts ...interp.RunnerOption) (*interp.Runner, error), bool, +) { + fn := d.getFunc(name) + if fn == nil { + return nil, false + } + + return func(ctx context.Context, opts ...interp.RunnerOption) (*interp.Runner, error) { + sub := d.Runner.Subshell() + for _, opt := range opts { + err := opt(sub) + if err != nil { + return nil, err + } + } + return sub, sub.Run(ctx, fn) + }, true +} + func (d *Decoder) getFunc(name string) *syntax.Stmt { names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) if err != nil { diff --git a/internal/translations/default.pot b/internal/translations/default.pot index b0cc59f..f2e3d0a 100644 --- a/internal/translations/default.pot +++ b/internal/translations/default.pot @@ -9,40 +9,48 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: build.go:41 +#: build.go:43 msgid "Build a local package" msgstr "" -#: build.go:47 +#: build.go:49 msgid "Path to the build script" msgstr "" -#: build.go:52 +#: build.go:54 msgid "Name of the package to build and its repo (example: default/go-bin)" msgstr "" -#: build.go:57 +#: build.go:59 msgid "" "Build package from scratch even if there's an already built package available" msgstr "" -#: build.go:87 +#: build.go:69 +msgid "Error db init" +msgstr "" + +#: build.go:102 msgid "Error pulling repositories" msgstr "" -#: build.go:95 +#: build.go:110 msgid "Unable to detect a supported package manager on the system" msgstr "" -#: build.go:107 +#: build.go:116 +msgid "Error parsing os release" +msgstr "" + +#: build.go:137 msgid "Error building package" msgstr "" -#: build.go:114 +#: build.go:144 msgid "Error getting working directory" msgstr "" -#: build.go:123 +#: build.go:153 msgid "Error moving the package" msgstr "" @@ -222,11 +230,11 @@ msgstr "" msgid "Error parsing system language" msgstr "" -#: internal/db/db.go:131 +#: internal/db/db.go:133 msgid "Database version mismatch; resetting" msgstr "" -#: internal/db/db.go:138 +#: internal/db/db.go:140 msgid "" "Database version does not exist. Run alr fix if something isn't working." msgstr "" @@ -293,82 +301,78 @@ msgstr "" msgid "Error while running app" msgstr "" -#: pkg/build/build.go:108 +#: pkg/build/build.go:116 msgid "Failed to prompt user to view build script" msgstr "" -#: pkg/build/build.go:112 +#: pkg/build/build.go:120 msgid "Building package" msgstr "" -#: pkg/build/build.go:156 +#: pkg/build/build.go:164 msgid "Downloading sources" msgstr "" -#: pkg/build/build.go:168 +#: pkg/build/build.go:176 msgid "Building package metadata" msgstr "" -#: pkg/build/build.go:190 +#: pkg/build/build.go:198 msgid "Compressing package" msgstr "" -#: pkg/build/build.go:316 +#: pkg/build/build.go:322 msgid "" "Your system's CPU architecture doesn't match this package. Do you want to " "build anyway?" msgstr "" -#: pkg/build/build.go:327 +#: pkg/build/build.go:336 msgid "This package is already installed" msgstr "" -#: pkg/build/build.go:355 +#: pkg/build/build.go:360 msgid "Installing build dependencies" msgstr "" -#: pkg/build/build.go:397 +#: pkg/build/build.go:371 msgid "Installing dependencies" msgstr "" -#: pkg/build/build.go:443 -msgid "Executing version()" +#: pkg/build/build.go:419 +msgid "The checksums array must be the same length as sources" msgstr "" -#: pkg/build/build.go:463 -msgid "Updating version" -msgstr "" - -#: pkg/build/build.go:468 -msgid "Executing prepare()" -msgstr "" - -#: pkg/build/build.go:478 -msgid "Executing build()" -msgstr "" - -#: pkg/build/build.go:488 -msgid "Executing package()" -msgstr "" - -#: pkg/build/build.go:510 -msgid "Executing files()" -msgstr "" - -#: pkg/build/build.go:588 -msgid "AutoProv is not implemented for this package format, so it's skipped" -msgstr "" - -#: pkg/build/build.go:599 -msgid "AutoReq is not implemented for this package format, so it's skipped" -msgstr "" - -#: pkg/build/build.go:706 +#: pkg/build/build.go:470 msgid "Would you like to remove the build dependencies?" msgstr "" -#: pkg/build/build.go:812 -msgid "The checksums array must be the same length as sources" +#: pkg/build/build.go:507 +msgid "Executing version()" +msgstr "" + +#: pkg/build/build.go:527 +msgid "Updating version" +msgstr "" + +#: pkg/build/build.go:532 +msgid "Executing prepare()" +msgstr "" + +#: pkg/build/build.go:542 +msgid "Executing build()" +msgstr "" + +#: pkg/build/build.go:558 pkg/build/build.go:586 +msgid "Executing %s()" +msgstr "" + +#: pkg/build/build_legacy.go:196 +msgid "AutoProv is not implemented for this package format, so it's skipped" +msgstr "" + +#: pkg/build/build_legacy.go:207 +msgid "AutoReq is not implemented for this package format, so it's skipped" msgstr "" #: pkg/build/findDeps.go:35 @@ -383,27 +387,27 @@ msgstr "" msgid "Required dependency found" msgstr "" -#: pkg/build/install.go:42 +#: pkg/build/install.go:44 msgid "Error installing native packages" msgstr "" -#: pkg/build/install.go:79 +#: pkg/build/install.go:94 msgid "Error installing package" msgstr "" -#: pkg/repos/pull.go:75 +#: pkg/repos/pull.go:79 msgid "Pulling repository" msgstr "" -#: pkg/repos/pull.go:99 +#: pkg/repos/pull.go:103 msgid "Repository up to date" msgstr "" -#: pkg/repos/pull.go:156 +#: pkg/repos/pull.go:160 msgid "Git repository does not appear to be a valid ALR repo" msgstr "" -#: pkg/repos/pull.go:172 +#: pkg/repos/pull.go:176 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index 3d5e548..de06d3b 100644 --- a/internal/translations/po/ru/default.po +++ b/internal/translations/po/ru/default.po @@ -16,40 +16,49 @@ msgstr "" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Gtranslator 47.1\n" -#: build.go:41 +#: build.go:43 msgid "Build a local package" msgstr "Сборка локального пакета" -#: build.go:47 +#: build.go:49 msgid "Path to the build script" msgstr "Путь к скрипту сборки" -#: build.go:52 +#: build.go:54 msgid "Name of the package to build and its repo (example: default/go-bin)" msgstr "Имя пакета для сборки и его репозиторий (пример: default/go-bin)" -#: build.go:57 +#: build.go:59 msgid "" "Build package from scratch even if there's an already built package available" msgstr "Создайте пакет с нуля, даже если уже имеется готовый пакет" -#: build.go:87 +#: build.go:69 +msgid "Error db init" +msgstr "" + +#: build.go:102 msgid "Error pulling repositories" msgstr "Ошибка при извлечении репозиториев" -#: build.go:95 +#: build.go:110 msgid "Unable to detect a supported package manager on the system" msgstr "Не удалось обнаружить поддерживаемый менеджер пакетов в системе" -#: build.go:107 +#: build.go:116 +#, fuzzy +msgid "Error parsing os release" +msgstr "Ошибка при разборе файла выпуска операционной системы" + +#: build.go:137 msgid "Error building package" msgstr "Ошибка при сборке пакета" -#: build.go:114 +#: build.go:144 msgid "Error getting working directory" msgstr "Ошибка при получении рабочего каталога" -#: build.go:123 +#: build.go:153 msgid "Error moving the package" msgstr "Ошибка при перемещении пакета" @@ -233,11 +242,11 @@ msgstr "Не удалось создать каталог кэша пакето msgid "Error parsing system language" msgstr "Ошибка при парсинге языка системы" -#: internal/db/db.go:131 +#: internal/db/db.go:133 msgid "Database version mismatch; resetting" msgstr "Несоответствие версий базы данных; сброс настроек" -#: internal/db/db.go:138 +#: internal/db/db.go:140 msgid "" "Database version does not exist. Run alr fix if something isn't working." msgstr "" @@ -307,27 +316,27 @@ msgstr "" msgid "Error while running app" msgstr "Ошибка при запуске приложения" -#: pkg/build/build.go:108 +#: pkg/build/build.go:116 msgid "Failed to prompt user to view build script" msgstr "Не удалось предложить пользователю просмотреть скрипт сборки" -#: pkg/build/build.go:112 +#: pkg/build/build.go:120 msgid "Building package" msgstr "Сборка пакета" -#: pkg/build/build.go:156 +#: pkg/build/build.go:164 msgid "Downloading sources" msgstr "Скачивание источников" -#: pkg/build/build.go:168 +#: pkg/build/build.go:176 msgid "Building package metadata" msgstr "Сборка метаданных пакета" -#: pkg/build/build.go:190 +#: pkg/build/build.go:198 msgid "Compressing package" msgstr "Сжатие пакета" -#: pkg/build/build.go:316 +#: pkg/build/build.go:322 msgid "" "Your system's CPU architecture doesn't match this package. Do you want to " "build anyway?" @@ -335,60 +344,57 @@ msgstr "" "Архитектура процессора вашей системы не соответствует этому пакету. Вы все " "равно хотите выполнить сборку?" -#: pkg/build/build.go:327 +#: pkg/build/build.go:336 msgid "This package is already installed" msgstr "Этот пакет уже установлен" -#: pkg/build/build.go:355 +#: pkg/build/build.go:360 msgid "Installing build dependencies" msgstr "Установка зависимостей сборки" -#: pkg/build/build.go:397 +#: pkg/build/build.go:371 msgid "Installing dependencies" msgstr "Установка зависимостей" -#: pkg/build/build.go:443 +#: pkg/build/build.go:419 +msgid "The checksums array must be the same length as sources" +msgstr "Массив контрольных сумм должен быть той же длины, что и источники" + +#: pkg/build/build.go:470 +msgid "Would you like to remove the build dependencies?" +msgstr "Хотели бы вы удалить зависимости сборки?" + +#: pkg/build/build.go:507 msgid "Executing version()" msgstr "Исполнение версия()" -#: pkg/build/build.go:463 +#: pkg/build/build.go:527 msgid "Updating version" msgstr "Обновление версии" -#: pkg/build/build.go:468 +#: pkg/build/build.go:532 msgid "Executing prepare()" msgstr "Исполнение prepare()" -#: pkg/build/build.go:478 +#: pkg/build/build.go:542 msgid "Executing build()" msgstr "Исполнение build()" -#: pkg/build/build.go:488 -msgid "Executing package()" -msgstr "Исполнение package()" - -#: pkg/build/build.go:510 -msgid "Executing files()" +#: pkg/build/build.go:558 pkg/build/build.go:586 +#, fuzzy +msgid "Executing %s()" msgstr "Исполнение files()" -#: pkg/build/build.go:588 +#: pkg/build/build_legacy.go:196 msgid "AutoProv is not implemented for this package format, so it's skipped" msgstr "" "AutoProv не реализовано для этого формата пакета, поэтому будет пропущено" -#: pkg/build/build.go:599 +#: pkg/build/build_legacy.go:207 msgid "AutoReq is not implemented for this package format, so it's skipped" msgstr "" "AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" -#: pkg/build/build.go:706 -msgid "Would you like to remove the build dependencies?" -msgstr "Хотели бы вы удалить зависимости сборки?" - -#: pkg/build/build.go:812 -msgid "The checksums array must be the same length as sources" -msgstr "Массив контрольных сумм должен быть той же длины, что и источники" - #: pkg/build/findDeps.go:35 msgid "Command not found on the system" msgstr "Команда не найдена в системе" @@ -401,27 +407,27 @@ msgstr "Найденная предоставленная зависимость msgid "Required dependency found" msgstr "Найдена требуемая зависимость" -#: pkg/build/install.go:42 +#: pkg/build/install.go:44 msgid "Error installing native packages" msgstr "Ошибка при установке нативных пакетов" -#: pkg/build/install.go:79 +#: pkg/build/install.go:94 msgid "Error installing package" msgstr "Ошибка при установке пакета" -#: pkg/repos/pull.go:75 +#: pkg/repos/pull.go:79 msgid "Pulling repository" msgstr "Скачивание репозитория" -#: pkg/repos/pull.go:99 +#: pkg/repos/pull.go:103 msgid "Repository up to date" msgstr "Репозиторий уже обновлён" -#: pkg/repos/pull.go:156 +#: pkg/repos/pull.go:160 msgid "Git repository does not appear to be a valid ALR repo" msgstr "Репозиторий Git не поддерживается репозиторием ALR" -#: pkg/repos/pull.go:172 +#: pkg/repos/pull.go:176 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." @@ -484,3 +490,6 @@ msgstr "Ошибка при проверке обновлений" #: upgrade.go:94 msgid "There is nothing to do." msgstr "Здесь нечего делать." + +#~ msgid "Executing package()" +#~ msgstr "Исполнение package()" diff --git a/internal/types/build.go b/internal/types/build.go index cef93a0..7682115 100644 --- a/internal/types/build.go +++ b/internal/types/build.go @@ -23,11 +23,61 @@ import "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager" type BuildOpts struct { Script string + Package string Manager manager.Manager Clean bool Interactive bool } +type BuildVarsPre struct { + Version string `sh:"version,required"` + Release int `sh:"release,required"` + Epoch uint `sh:"epoch"` + Description string `sh:"desc"` + Homepage string `sh:"homepage"` + Maintainer string `sh:"maintainer"` + Architectures []string `sh:"architectures"` + Licenses []string `sh:"license"` + Provides []string `sh:"provides"` + Conflicts []string `sh:"conflicts"` + Depends []string `sh:"deps"` + BuildDepends []string `sh:"build_deps"` + OptDepends []string `sh:"opt_deps"` + Replaces []string `sh:"replaces"` + Sources []string `sh:"sources"` + Checksums []string `sh:"checksums"` + Backup []string `sh:"backup"` + Scripts Scripts `sh:"scripts"` + AutoReq []string `sh:"auto_req"` + AutoProv []string `sh:"auto_prov"` +} + +func (bv *BuildVarsPre) ToBuildVars() BuildVars { + return BuildVars{ + Name: "", + Version: bv.Version, + Release: bv.Release, + Epoch: bv.Epoch, + Description: bv.Description, + Homepage: bv.Homepage, + Maintainer: bv.Maintainer, + Architectures: bv.Architectures, + Licenses: bv.Licenses, + Provides: bv.Provides, + Conflicts: bv.Conflicts, + Depends: bv.Depends, + BuildDepends: bv.BuildDepends, + OptDepends: bv.OptDepends, + Replaces: bv.Replaces, + Sources: bv.Sources, + Checksums: bv.Checksums, + Backup: bv.Backup, + Scripts: bv.Scripts, + AutoReq: bv.AutoReq, + AutoProv: bv.AutoProv, + } +} + // BuildVars represents the script variables required // to build a package type BuildVars struct { diff --git a/pkg/build/build.go b/pkg/build/build.go index 62be0a0..9125530 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -23,39 +23,26 @@ import ( "bytes" "context" "encoding/hex" + "errors" "fmt" - "io" "log/slog" "os" "path/filepath" - "runtime" - "slices" - "strconv" "strings" "time" - // Импортируем пакеты для поддержки различных форматов пакетов (APK, DEB, RPM и ARCH). - "github.com/google/shlex" - _ "github.com/goreleaser/nfpm/v2/apk" - _ "github.com/goreleaser/nfpm/v2/arch" - _ "github.com/goreleaser/nfpm/v2/deb" - _ "github.com/goreleaser/nfpm/v2/rpm" + "github.com/goreleaser/nfpm/v2" "github.com/leonelquinteros/gotext" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" - "github.com/goreleaser/nfpm/v2" - "github.com/goreleaser/nfpm/v2/files" - "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu" - "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" "gitea.plemya-x.ru/Plemya-x/ALR/internal/dl" "gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache" - "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/helpers" @@ -65,34 +52,49 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos" ) -// Функция BuildPackage выполняет сборку скрипта по указанному пути. Возвращает два среза. -// Один содержит пути к собранным пакетам, другой - имена собранных пакетов. -func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string, error) { - reposInstance := repos.GetInstance(ctx) +type Builder struct { + ctx context.Context + opts types.BuildOpts + info *distro.OSRelease + repos *repos.Repos + config *config.ALRConfig +} - info, err := distro.ParseOSRelease(ctx) - if err != nil { - return nil, nil, err +func New( + ctx context.Context, + opts types.BuildOpts, + repos *repos.Repos, + info *distro.OSRelease, + config *config.ALRConfig, +) *Builder { + return &Builder{ + ctx: ctx, + opts: opts, + info: info, + repos: repos, + config: config, } +} - fl, err := parseScript(info, opts.Script) +func (b *Builder) BuildPackage(ctx context.Context) ([]string, []string, error) { + fl, err := readScript(b.opts.Script) if err != nil { return nil, nil, err } // Первый проход предназначен для получения значений переменных и выполняется // до отображения скрипта, чтобы предотвратить выполнение вредоносного кода. - vars, err := executeFirstPass(ctx, info, fl, opts.Script) + vars, err := b.executeFirstPass(fl) if err != nil { return nil, nil, err } - dirs := getDirs(ctx, vars, opts.Script) + dirs := b.getDirs(vars) // Если флаг opts.Clean не установлен, и пакет уже собран, // возвращаем его, а не собираем заново. - if !opts.Clean { - builtPkgPath, ok, err := checkForBuiltPackage(opts.Manager, vars, getPkgFormat(opts.Manager), dirs.BaseDir) + if !b.opts.Clean { + builtPkgPath, ok, err := checkForBuiltPackage(b.opts.Manager, vars, getPkgFormat(b.opts.Manager), dirs.BaseDir) if err != nil { return nil, nil, err } @@ -103,7 +105,13 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string } // Спрашиваем у пользователя, хочет ли он увидеть скрипт сборки. - err = cliutils.PromptViewScript(ctx, opts.Script, vars.Name, config.Config(ctx).PagerStyle, opts.Interactive) + err = cliutils.PromptViewScript( + ctx, + b.opts.Script, + vars.Name, + b.config.PagerStyle(ctx), + b.opts.Interactive, + ) if err != nil { slog.Error(gotext.Get("Failed to prompt user to view build script"), "err", err) os.Exit(1) @@ -114,18 +122,18 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string // Второй проход будет использоваться для выполнения реального кода, // поэтому он не ограничен. Скрипт уже был показан // пользователю к этому моменту, так что это должно быть безопасно. - dec, err := executeSecondPass(ctx, info, fl, dirs) + dec, err := b.executeSecondPass(ctx, fl, dirs) if err != nil { return nil, nil, err } // Получаем список установленных пакетов в системе - installed, err := opts.Manager.ListInstalled(nil) + installed, err := b.opts.Manager.ListInstalled(nil) if err != nil { return nil, nil, err } - cont, err := performChecks(ctx, vars, opts.Interactive, installed) // Выполняем различные проверки + cont, err := b.performChecks(ctx, vars, installed) // Выполняем различные проверки if err != nil { return nil, nil, err } else if !cont { @@ -138,38 +146,38 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string return nil, nil, err } - buildDeps, err := installBuildDeps(ctx, reposInstance, vars, opts) // Устанавливаем зависимости для сборки + buildDeps, err := b.installBuildDeps(ctx, vars) // Устанавливаем зависимости для сборки if err != nil { return nil, nil, err } - err = installOptDeps(ctx, reposInstance, vars, opts) // Устанавливаем опциональные зависимости + err = installOptDeps(ctx, b.repos, vars, b.opts) // Устанавливаем опциональные зависимости if err != nil { return nil, nil, err } - builtPaths, builtNames, repoDeps, err := buildALRDeps(ctx, opts, vars) // Собираем зависимости + builtPaths, builtNames, repoDeps, err := b.buildALRDeps(ctx, vars) // Собираем зависимости if err != nil { return nil, nil, err } slog.Info(gotext.Get("Downloading sources")) // Записываем в лог загрузку источников - err = getSources(ctx, dirs, vars) // Загружаем исходники + err = b.getSources(ctx, dirs, vars) // Загружаем исходники if err != nil { return nil, nil, err } - funcOut, err := executeFunctions(ctx, dec, dirs, vars) // Выполняем специальные функции + funcOut, err := b.executeFunctions(ctx, dec, dirs, vars) // Выполняем специальные функции if err != nil { return nil, nil, err } slog.Info(gotext.Get("Building package metadata"), "name", vars.Name) - pkgFormat := getPkgFormat(opts.Manager) // Получаем формат пакета + pkgFormat := getPkgFormat(b.opts.Manager) // Получаем формат пакета - pkgInfo, err := buildPkgMetadata(ctx, vars, dirs, pkgFormat, info, append(repoDeps, builtNames...), funcOut.Contents) // Собираем метаданные пакета + pkgInfo, err := buildPkgMetadata(ctx, vars, dirs, pkgFormat, b.info, append(repoDeps, builtNames...), funcOut.Contents) // Собираем метаданные пакета if err != nil { return nil, nil, err } @@ -194,7 +202,7 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string return nil, nil, err } - err = removeBuildDeps(ctx, buildDeps, opts) // Удаляем зависимости для сборки + err = b.removeBuildDeps(ctx, buildDeps) // Удаляем зависимости для сборки if err != nil { return nil, nil, err } @@ -213,27 +221,13 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string return pkgPaths, pkgNames, nil // Возвращаем пути и имена пакетов } -// Функция parseScript анализирует скрипт сборки с использованием встроенной реализации bash -func parseScript(info *distro.OSRelease, script string) (*syntax.File, error) { - fl, err := os.Open(script) // Открываем файл скрипта - if err != nil { - return nil, err - } - defer fl.Close() // Закрываем файл после выполнения - - file, err := syntax.NewParser().Parse(fl, "alr.sh") // Парсим скрипт с помощью синтаксического анализатора - if err != nil { - return nil, err - } - - return file, nil // Возвращаем синтаксическое дерево -} - // Функция executeFirstPass выполняет парсированный скрипт в ограниченной среде, // чтобы извлечь переменные сборки без выполнения реального кода. -func executeFirstPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, script string) (*types.BuildVars, error) { - scriptDir := filepath.Dir(script) // Получаем директорию скрипта - env := createBuildEnvVars(info, types.Directories{ScriptDir: scriptDir}) // Создаём переменные окружения для сборки +func (b *Builder) executeFirstPass( + fl *syntax.File, +) (*types.BuildVars, error) { + scriptDir := filepath.Dir(b.opts.Script) // Получаем директорию скрипта + env := createBuildEnvVars(b.info, types.Directories{ScriptDir: scriptDir}) // Создаём переменные окружения для сборки runner, err := interp.New( interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение @@ -247,37 +241,60 @@ func executeFirstPass(ctx context.Context, info *distro.OSRelease, fl *syntax.Fi return nil, err } - err = runner.Run(ctx, fl) // Запускаем скрипт + err = runner.Run(b.ctx, fl) // Запускаем скрипт if err != nil { return nil, err } - dec := decoder.New(info, runner) // Создаём новый декодер - + dec := decoder.New(b.info, runner) // Создаём новый декодер var vars types.BuildVars - err = dec.DecodeVars(&vars) // Декодируем переменные + if b.opts.Package == "" { + err = dec.DecodeVars(&vars) // Декодируем переменные + if err != nil { + return nil, err + } + return &vars, nil + } + var preVars types.BuildVarsPre + funcName := fmt.Sprintf("meta_%s", b.opts.Package) + meta, ok := dec.GetFuncSub(funcName) + if !ok { + return nil, errors.New("func is missing") + } + r, err := meta(b.ctx) if err != nil { return nil, err } + d := decoder.New(&distro.OSRelease{}, r) + err = d.DecodeVars(&preVars) + if err != nil { + return nil, err + } + vars = preVars.ToBuildVars() + vars.Name = b.opts.Package return &vars, nil // Возвращаем переменные сборки } // Функция getDirs возвращает соответствующие директории для скрипта -func getDirs(ctx context.Context, vars *types.BuildVars, script string) types.Directories { - baseDir := filepath.Join(config.GetPaths(ctx).PkgsDir, vars.Name) // Определяем базовую директорию +func (b *Builder) getDirs(vars *types.BuildVars) types.Directories { + baseDir := filepath.Join(b.config.GetPaths(b.ctx).PkgsDir, vars.Name) // Определяем базовую директорию return types.Directories{ BaseDir: baseDir, SrcDir: filepath.Join(baseDir, "src"), PkgDir: filepath.Join(baseDir, "pkg"), - ScriptDir: filepath.Dir(script), + ScriptDir: filepath.Dir(b.opts.Script), } } // Функция executeSecondPass выполняет скрипт сборки второй раз без каких-либо ограничений. Возвращается декодер, // который может быть использован для получения функций и переменных из скрипта. -func executeSecondPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, dirs types.Directories) (*decoder.Decoder, error) { - env := createBuildEnvVars(info, dirs) // Создаём переменные окружения для сборки +func (b *Builder) executeSecondPass( + ctx context.Context, + fl *syntax.File, + dirs types.Directories, +) (*decoder.Decoder, error) { + env := createBuildEnvVars(b.info, dirs) // Создаём переменные окружения для сборки fakeroot := handlers.FakerootExecHandler(2 * time.Second) // Настраиваем "fakeroot" для выполнения runner, err := interp.New( @@ -294,26 +311,18 @@ func executeSecondPass(ctx context.Context, info *distro.OSRelease, fl *syntax.F return nil, err } - return decoder.New(info, runner), nil // Возвращаем новый декодер -} - -// Функция prepareDirs подготавливает директории для сборки. -func prepareDirs(dirs types.Directories) error { - err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует - if err != nil { - return err - } - err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников - if err != nil { - return err - } - return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов + return decoder.New(b.info, runner), nil // Возвращаем новый декодер } // Функция performChecks проверяет различные аспекты в системе, чтобы убедиться, что пакет может быть установлен. -func performChecks(ctx context.Context, vars *types.BuildVars, interactive bool, installed map[string]string) (bool, error) { +func (b *Builder) performChecks(ctx context.Context, vars *types.BuildVars, installed map[string]string) (bool, error) { if !cpu.IsCompatibleWith(cpu.Arch(), vars.Architectures) { // Проверяем совместимость архитектуры - cont, err := cliutils.YesNoPrompt(ctx, gotext.Get("Your system's CPU architecture doesn't match this package. Do you want to build anyway?"), interactive, true) + cont, err := cliutils.YesNoPrompt( + ctx, + gotext.Get("Your system's CPU architecture doesn't match this package. Do you want to build anyway?"), + b.opts.Interactive, + true, + ) if err != nil { return false, err } @@ -333,16 +342,12 @@ func performChecks(ctx context.Context, vars *types.BuildVars, interactive bool, return true, nil } -type PackageFinder interface { - FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) -} - // Функция installBuildDeps устанавливает все зависимости сборки, которые еще не установлены, и возвращает // срез, содержащий имена всех установленных пакетов. -func installBuildDeps(ctx context.Context, repos PackageFinder, vars *types.BuildVars, opts types.BuildOpts) ([]string, error) { +func (b *Builder) installBuildDeps(ctx context.Context, vars *types.BuildVars) ([]string, error) { var buildDeps []string if len(vars.BuildDepends) > 0 { - deps, err := removeAlreadyInstalled(opts, vars.BuildDepends) + deps, err := removeAlreadyInstalled(b.opts, vars.BuildDepends) if err != nil { return nil, err } @@ -354,45 +359,14 @@ func installBuildDeps(ctx context.Context, repos PackageFinder, vars *types.Buil slog.Info(gotext.Get("Installing build dependencies")) // Логгируем установку зависимостей - flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) // Уплощаем список зависимостей + flattened := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive) // Уплощаем список зависимостей buildDeps = packageNames(flattened) - InstallPkgs(ctx, flattened, notFound, opts) // Устанавливаем пакеты + InstallPkgs(ctx, flattened, notFound, b.opts) // Устанавливаем пакеты } return buildDeps, nil } -// Функция installOptDeps спрашивает у пользователя, какие, если таковые имеются, опциональные зависимости он хочет установить. -// Если пользователь решает установить какие-либо опциональные зависимости, выполняется их установка. -func installOptDeps(ctx context.Context, repos PackageFinder, vars *types.BuildVars, opts types.BuildOpts) error { - optDeps, err := removeAlreadyInstalled(opts, vars.OptDepends) - if err != nil { - return err - } - if len(optDeps) > 0 { - optDeps, err := cliutils.ChooseOptDepends(ctx, optDeps, "install", opts.Interactive) // Пользователя просят выбрать опциональные зависимости - if err != nil { - return err - } - - if len(optDeps) == 0 { - return nil - } - - found, notFound, err := repos.FindPkgs(ctx, optDeps) // Находим опциональные зависимости - if err != nil { - return err - } - - flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) - InstallPkgs(ctx, flattened, notFound, opts) // Устанавливаем выбранные пакеты - } - return nil -} - -// Функция buildALRDeps собирает все ALR зависимости пакета. Возвращает пути и имена -// пакетов, которые она собрала, а также все зависимости, которые не были найдены в ALR репозитории, -// чтобы они могли быть установлены из системных репозиториев. -func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVars) (builtPaths, builtNames, repoDeps []string, err error) { +func (b *Builder) buildALRDeps(ctx context.Context, vars *types.BuildVars) (builtPaths, builtNames, repoDeps []string, err error) { if len(vars.Depends) > 0 { slog.Info(gotext.Get("Installing dependencies")) @@ -403,14 +377,22 @@ func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVa repoDeps = notFound // Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез - pkgs := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) - scripts := GetScriptPaths(ctx, pkgs) - for _, script := range scripts { - newOpts := opts - newOpts.Script = script + pkgs := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive) + + for _, pkg := range pkgs { + newOpts := b.opts + UpdateOpts(ctx, &newOpts, &pkg) + + newB := New( + ctx, + newOpts, + b.repos, + b.info, + b.config, + ) // Собираем зависимости - pkgPaths, pkgNames, err := BuildPackage(ctx, newOpts) + pkgPaths, pkgNames, err := newB.BuildPackage(ctx) if err != nil { return nil, nil, nil, err } @@ -420,7 +402,7 @@ func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVa // Добавляем пути всех собранных пакетов в builtPaths builtNames = append(builtNames, pkgNames...) // Добавляем имя текущего пакета в builtNames - builtNames = append(builtNames, filepath.Base(filepath.Dir(script))) + builtNames = append(builtNames, filepath.Base(filepath.Dir(newOpts.Script))) } } @@ -432,12 +414,94 @@ func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVa return builtPaths, builtNames, repoDeps, nil } +func (b *Builder) getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars) error { + if len(bv.Sources) != len(bv.Checksums) { + slog.Error(gotext.Get("The checksums array must be the same length as sources")) + os.Exit(1) + } + + for i, src := range bv.Sources { + opts := dl.Options{ + Name: fmt.Sprintf("%s[%d]", bv.Name, i), + URL: src, + Destination: dirs.SrcDir, + Progress: os.Stderr, + LocalDir: dirs.ScriptDir, + } + + if !strings.EqualFold(bv.Checksums[i], "SKIP") { + // Если контрольная сумма содержит двоеточие, используйте часть до двоеточия + // как алгоритм, а часть после как фактическую контрольную сумму. + // В противном случае используйте sha256 по умолчанию с целой строкой как контрольной суммой. + algo, hashData, ok := strings.Cut(bv.Checksums[i], ":") + if ok { + checksum, err := hex.DecodeString(hashData) + if err != nil { + return err + } + opts.Hash = checksum + opts.HashAlgorithm = algo + } else { + checksum, err := hex.DecodeString(bv.Checksums[i]) + if err != nil { + return err + } + opts.Hash = checksum + } + } + + opts.DlCache = dlcache.New(b.config) + + err := dl.Download(ctx, opts) + if err != nil { + return err + } + } + + return nil +} + +// Функция removeBuildDeps спрашивает у пользователя, хочет ли он удалить зависимости, +// установленные для сборки. Если да, использует менеджер пакетов для их удаления. +func (b *Builder) removeBuildDeps(ctx context.Context, buildDeps []string) error { + if len(buildDeps) > 0 { + remove, err := cliutils.YesNoPrompt( + ctx, + gotext.Get("Would you like to remove the build dependencies?"), + b.opts.Interactive, + false, + ) + if err != nil { + return err + } + + if remove { + err = b.opts.Manager.Remove( + &manager.Opts{ + AsRoot: true, + NoConfirm: true, + }, + buildDeps..., + ) + if err != nil { + return err + } + } + } + return nil +} + type FunctionsOutput struct { Contents *[]string } // Функция executeFunctions выполняет специальные функции ALR, такие как version(), prepare() и т.д. -func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Directories, vars *types.BuildVars) (*FunctionsOutput, error) { +func (b *Builder) executeFunctions( + ctx context.Context, + dec *decoder.Decoder, + dirs types.Directories, + vars *types.BuildVars, +) (*FunctionsOutput, error) { version, ok := dec.GetFunc("version") if ok { slog.Info(gotext.Get("Executing version()")) @@ -483,9 +547,15 @@ func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Dire } } - packageFn, ok := dec.GetFunc("package") + var packageFuncName string + if b.opts.Package == "" { + packageFuncName = "package" + } else { + packageFuncName = fmt.Sprintf("package_%s", b.opts.Package) + } + packageFn, ok := dec.GetFunc(packageFuncName) if ok { - slog.Info(gotext.Get("Executing package()")) + slog.Info(gotext.Get("Executing %s()", packageFuncName)) err := packageFn(ctx, interp.Dir(dirs.SrcDir)) if err != nil { return nil, err @@ -494,7 +564,13 @@ func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Dire output := &FunctionsOutput{} - files, ok := dec.GetFuncP("files", func(ctx context.Context, s *interp.Runner) error { + var filesFuncName string + if b.opts.Package == "" { + filesFuncName = "files" + } else { + filesFuncName = fmt.Sprintf("files_%s", b.opts.Package) + } + files, ok := dec.GetFuncP(filesFuncName, func(ctx context.Context, s *interp.Runner) error { // It should be done via interp.RunnerOption, // but due to the issues below, it cannot be done. // - https://github.com/mvdan/sh/issues/962 @@ -507,7 +583,7 @@ func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Dire }) if ok { - slog.Info(gotext.Get("Executing files()")) + slog.Info(gotext.Get("Executing %s()", filesFuncName)) buf := &bytes.Buffer{} @@ -529,417 +605,3 @@ func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Dire return output, nil } - -// Функция buildPkgMetadata создает метаданные для пакета, который будет собран. -func buildPkgMetadata( - ctx context.Context, - vars *types.BuildVars, - dirs types.Directories, - pkgFormat string, - info *distro.OSRelease, - deps []string, - preferedContents *[]string, -) (*nfpm.Info, error) { - pkgInfo := getBasePkgInfo(vars) - pkgInfo.Description = vars.Description - pkgInfo.Platform = "linux" - pkgInfo.Homepage = vars.Homepage - pkgInfo.License = strings.Join(vars.Licenses, ", ") - pkgInfo.Maintainer = vars.Maintainer - pkgInfo.Overridables = nfpm.Overridables{ - Conflicts: vars.Conflicts, - Replaces: vars.Replaces, - Provides: vars.Provides, - Depends: deps, - } - - if pkgFormat == "apk" { - // Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы - pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool { - return s == pkgInfo.Name - }) - } - - pkgInfo.Release = overrides.ReleasePlatformSpecific(vars.Release, info) - - if vars.Epoch != 0 { - pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10) - } - - setScripts(vars, pkgInfo, dirs.ScriptDir) - - if slices.Contains(vars.Architectures, "all") { - pkgInfo.Arch = "all" - } - - contents, err := buildContents(vars, dirs, preferedContents) - if err != nil { - return nil, err - } - pkgInfo.Overridables.Contents = contents - - if len(vars.AutoProv) == 1 && decoder.IsTruthy(vars.AutoProv[0]) { - if pkgFormat == "rpm" { - err = rpmFindProvides(ctx, pkgInfo, dirs) - if err != nil { - return nil, err - } - } else { - slog.Info(gotext.Get("AutoProv is not implemented for this package format, so it's skipped")) - } - } - - if len(vars.AutoReq) == 1 && decoder.IsTruthy(vars.AutoReq[0]) { - if pkgFormat == "rpm" { - err = rpmFindRequires(ctx, pkgInfo, dirs) - if err != nil { - return nil, err - } - } else { - slog.Info(gotext.Get("AutoReq is not implemented for this package format, so it's skipped")) - } - } - - return pkgInfo, nil -} - -// Функция buildContents создает секцию содержимого пакета, которая содержит файлы, -// которые будут включены в конечный пакет. -func buildContents(vars *types.BuildVars, dirs types.Directories, preferedContents *[]string) ([]*files.Content, error) { - contents := []*files.Content{} - - processPath := func(path, trimmed string, prefered bool) error { - fi, err := os.Lstat(path) - if err != nil { - return err - } - - if fi.IsDir() { - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() - - if !prefered { - _, err = f.Readdirnames(1) - if err != io.EOF { - return nil - } - } - - contents = append(contents, &files.Content{ - Source: path, - Destination: trimmed, - Type: "dir", - FileInfo: &files.ContentFileInfo{ - MTime: fi.ModTime(), - }, - }) - return nil - } - - if fi.Mode()&os.ModeSymlink != 0 { - link, err := os.Readlink(path) - if err != nil { - return err - } - link = strings.TrimPrefix(link, dirs.PkgDir) - - contents = append(contents, &files.Content{ - Source: link, - Destination: trimmed, - Type: "symlink", - FileInfo: &files.ContentFileInfo{ - MTime: fi.ModTime(), - Mode: fi.Mode(), - }, - }) - return nil - } - - fileContent := &files.Content{ - Source: path, - Destination: trimmed, - FileInfo: &files.ContentFileInfo{ - MTime: fi.ModTime(), - Mode: fi.Mode(), - Size: fi.Size(), - }, - } - - if slices.Contains(vars.Backup, trimmed) { - fileContent.Type = "config|noreplace" - } - - contents = append(contents, fileContent) - return nil - } - - if preferedContents != nil { - for _, trimmed := range *preferedContents { - path := filepath.Join(dirs.PkgDir, trimmed) - if err := processPath(path, trimmed, true); err != nil { - return nil, err - } - } - } else { - err := filepath.Walk(dirs.PkgDir, func(path string, fi os.FileInfo, err error) error { - if err != nil { - return err - } - trimmed := strings.TrimPrefix(path, dirs.PkgDir) - return processPath(path, trimmed, false) - }) - if err != nil { - return nil, err - } - } - - return contents, nil -} - -// Функция removeBuildDeps спрашивает у пользователя, хочет ли он удалить зависимости, -// установленные для сборки. Если да, использует менеджер пакетов для их удаления. -func removeBuildDeps(ctx context.Context, buildDeps []string, opts types.BuildOpts) error { - if len(buildDeps) > 0 { - remove, err := cliutils.YesNoPrompt(ctx, gotext.Get("Would you like to remove the build dependencies?"), opts.Interactive, false) - if err != nil { - return err - } - - if remove { - err = opts.Manager.Remove( - &manager.Opts{ - AsRoot: true, - NoConfirm: true, - }, - buildDeps..., - ) - if err != nil { - return err - } - } - } - return nil -} - -// Функция checkForBuiltPackage пытается обнаружить ранее собранный пакет и вернуть его путь -// и true, если нашла. Если нет, возвратит "", false, nil. -func checkForBuiltPackage(mgr manager.Manager, vars *types.BuildVars, pkgFormat, baseDir string) (string, bool, error) { - filename, err := pkgFileName(vars, pkgFormat) - if err != nil { - return "", false, err - } - - pkgPath := filepath.Join(baseDir, filename) - - _, err = os.Stat(pkgPath) - if err != nil { - return "", false, nil - } - - return pkgPath, true, nil -} - -func getBasePkgInfo(vars *types.BuildVars) *nfpm.Info { - return &nfpm.Info{ - Name: vars.Name, - Arch: cpu.Arch(), - Version: vars.Version, - Release: strconv.Itoa(vars.Release), - Epoch: strconv.FormatUint(uint64(vars.Epoch), 10), - } -} - -// pkgFileName returns the filename of the package if it were to be built. -// This is used to check if the package has already been built. -func pkgFileName(vars *types.BuildVars, pkgFormat string) (string, error) { - pkgInfo := getBasePkgInfo(vars) - - packager, err := nfpm.Get(pkgFormat) - if err != nil { - return "", err - } - - return packager.ConventionalFileName(pkgInfo), nil -} - -// Функция getPkgFormat возвращает формат пакета из менеджера пакетов, -// или ALR_PKG_FORMAT, если он установлен. -func getPkgFormat(mgr manager.Manager) string { - pkgFormat := mgr.Format() - if format, ok := os.LookupEnv("ALR_PKG_FORMAT"); ok { - pkgFormat = format - } - return pkgFormat -} - -// Функция createBuildEnvVars создает переменные окружения, которые будут установлены -// в скрипте сборки при его выполнении. -func createBuildEnvVars(info *distro.OSRelease, dirs types.Directories) []string { - env := os.Environ() - - env = append( - env, - "DISTRO_NAME="+info.Name, - "DISTRO_PRETTY_NAME="+info.PrettyName, - "DISTRO_ID="+info.ID, - "DISTRO_VERSION_ID="+info.VersionID, - "DISTRO_ID_LIKE="+strings.Join(info.Like, " "), - "ARCH="+cpu.Arch(), - "NCPU="+strconv.Itoa(runtime.NumCPU()), - ) - - if dirs.ScriptDir != "" { - env = append(env, "scriptdir="+dirs.ScriptDir) - } - - if dirs.PkgDir != "" { - env = append(env, "pkgdir="+dirs.PkgDir) - } - - if dirs.SrcDir != "" { - env = append(env, "srcdir="+dirs.SrcDir) - } - - return env -} - -// Функция getSources загружает исходники скрипта. -func getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars) error { - if len(bv.Sources) != len(bv.Checksums) { - slog.Error(gotext.Get("The checksums array must be the same length as sources")) - os.Exit(1) - } - - for i, src := range bv.Sources { - opts := dl.Options{ - Name: fmt.Sprintf("%s[%d]", bv.Name, i), - URL: src, - Destination: dirs.SrcDir, - Progress: os.Stderr, - LocalDir: dirs.ScriptDir, - } - - if !strings.EqualFold(bv.Checksums[i], "SKIP") { - // Если контрольная сумма содержит двоеточие, используйте часть до двоеточия - // как алгоритм, а часть после как фактическую контрольную сумму. - // В противном случае используйте sha256 по умолчанию с целой строкой как контрольной суммой. - algo, hashData, ok := strings.Cut(bv.Checksums[i], ":") - if ok { - checksum, err := hex.DecodeString(hashData) - if err != nil { - return err - } - opts.Hash = checksum - opts.HashAlgorithm = algo - } else { - checksum, err := hex.DecodeString(bv.Checksums[i]) - if err != nil { - return err - } - opts.Hash = checksum - } - } - - cfg := config.GetInstance(ctx) - opts.DlCache = dlcache.New(cfg) - - err := dl.Download(ctx, opts) - if err != nil { - return err - } - } - - return nil -} - -// Функция setScripts добавляет скрипты-перехватчики к метаданным пакета. -func setScripts(vars *types.BuildVars, info *nfpm.Info, scriptDir string) { - if vars.Scripts.PreInstall != "" { - info.Scripts.PreInstall = filepath.Join(scriptDir, vars.Scripts.PreInstall) - } - - if vars.Scripts.PostInstall != "" { - info.Scripts.PostInstall = filepath.Join(scriptDir, vars.Scripts.PostInstall) - } - - if vars.Scripts.PreRemove != "" { - info.Scripts.PreRemove = filepath.Join(scriptDir, vars.Scripts.PreRemove) - } - - if vars.Scripts.PostRemove != "" { - info.Scripts.PostRemove = filepath.Join(scriptDir, vars.Scripts.PostRemove) - } - - if vars.Scripts.PreUpgrade != "" { - info.ArchLinux.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade) - info.APK.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade) - } - - if vars.Scripts.PostUpgrade != "" { - info.ArchLinux.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade) - info.APK.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade) - } - - if vars.Scripts.PreTrans != "" { - info.RPM.Scripts.PreTrans = filepath.Join(scriptDir, vars.Scripts.PreTrans) - } - - if vars.Scripts.PostTrans != "" { - info.RPM.Scripts.PostTrans = filepath.Join(scriptDir, vars.Scripts.PostTrans) - } -} - -// Функция setVersion изменяет переменную версии в скрипте runner. -// Она используется для установки версии на вывод функции version(). -func setVersion(ctx context.Context, r *interp.Runner, to string) error { - fl, err := syntax.NewParser().Parse(strings.NewReader("version='"+to+"'"), "") - if err != nil { - return err - } - return r.Run(ctx, fl) -} - -// Returns not installed dependencies -func removeAlreadyInstalled(opts types.BuildOpts, dependencies []string) ([]string, error) { - filteredPackages := []string{} - - for _, dep := range dependencies { - installed, err := opts.Manager.IsInstalled(dep) - if err != nil { - return nil, err - } - if installed { - continue - } - filteredPackages = append(filteredPackages, dep) - } - - return filteredPackages, nil -} - -// Функция packageNames возвращает имена всех предоставленных пакетов. -func packageNames(pkgs []db.Package) []string { - names := make([]string, len(pkgs)) - for i, p := range pkgs { - names[i] = p.Name - } - return names -} - -// Функция removeDuplicates убирает любые дубликаты из предоставленного среза. -func removeDuplicates(slice []string) []string { - seen := map[string]struct{}{} - result := []string{} - - for _, s := range slice { - if _, ok := seen[s]; !ok { - seen[s] = struct{}{} - result = append(result, s) - } - } - - return result -} diff --git a/pkg/build/build_legacy.go b/pkg/build/build_legacy.go new file mode 100644 index 0000000..aee4286 --- /dev/null +++ b/pkg/build/build_legacy.go @@ -0,0 +1,479 @@ +// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan. +// It has been modified as part of "ALR - Any Linux Repository" by Евгений Храмов. +// +// ALR - Any Linux Repository +// Copyright (C) 2025 Евгений Храмов +// +// 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 . + +package build + +import ( + "context" + "io" + "log/slog" + "os" + "path/filepath" + "runtime" + "slices" + "strconv" + "strings" + + // Импортируем пакеты для поддержки различных форматов пакетов (APK, DEB, RPM и ARCH). + + _ "github.com/goreleaser/nfpm/v2/apk" + _ "github.com/goreleaser/nfpm/v2/arch" + _ "github.com/goreleaser/nfpm/v2/deb" + _ "github.com/goreleaser/nfpm/v2/rpm" + "github.com/leonelquinteros/gotext" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" + + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + + "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/types" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager" +) + +// Функция readScript анализирует скрипт сборки с использованием встроенной реализации bash +func readScript(script string) (*syntax.File, error) { + fl, err := os.Open(script) // Открываем файл скрипта + if err != nil { + return nil, err + } + defer fl.Close() // Закрываем файл после выполнения + + file, err := syntax.NewParser().Parse(fl, "alr.sh") // Парсим скрипт с помощью синтаксического анализатора + if err != nil { + return nil, err + } + + return file, nil // Возвращаем синтаксическое дерево +} + +// Функция prepareDirs подготавливает директории для сборки. +func prepareDirs(dirs types.Directories) error { + err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует + if err != nil { + return err + } + err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников + if err != nil { + return err + } + return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов +} + +type PackageFinder interface { + FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) +} + +// Функция installBuildDeps устанавливает все зависимости сборки, которые еще не установлены, и возвращает +// срез, содержащий имена всех установленных пакетов. +func installBuildDeps(ctx context.Context, repos PackageFinder, vars *types.BuildVars, opts types.BuildOpts) ([]string, error) { + var buildDeps []string + if len(vars.BuildDepends) > 0 { + deps, err := removeAlreadyInstalled(opts, vars.BuildDepends) + if err != nil { + return nil, err + } + + found, notFound, err := repos.FindPkgs(ctx, deps) // Находим пакеты-зависимости + if err != nil { + return nil, err + } + + slog.Info(gotext.Get("Installing build dependencies")) // Логгируем установку зависимостей + + flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) // Уплощаем список зависимостей + buildDeps = packageNames(flattened) + InstallPkgs(ctx, flattened, notFound, opts) // Устанавливаем пакеты + } + return buildDeps, nil +} + +// Функция installOptDeps спрашивает у пользователя, какие, если таковые имеются, опциональные зависимости он хочет установить. +// Если пользователь решает установить какие-либо опциональные зависимости, выполняется их установка. +func installOptDeps(ctx context.Context, repos PackageFinder, vars *types.BuildVars, opts types.BuildOpts) error { + optDeps, err := removeAlreadyInstalled(opts, vars.OptDepends) + if err != nil { + return err + } + if len(optDeps) > 0 { + optDeps, err := cliutils.ChooseOptDepends(ctx, optDeps, "install", opts.Interactive) // Пользователя просят выбрать опциональные зависимости + if err != nil { + return err + } + + if len(optDeps) == 0 { + return nil + } + + found, notFound, err := repos.FindPkgs(ctx, optDeps) // Находим опциональные зависимости + if err != nil { + return err + } + + flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) + InstallPkgs(ctx, flattened, notFound, opts) // Устанавливаем выбранные пакеты + } + return nil +} + +// Функция buildPkgMetadata создает метаданные для пакета, который будет собран. +func buildPkgMetadata( + ctx context.Context, + vars *types.BuildVars, + dirs types.Directories, + pkgFormat string, + info *distro.OSRelease, + deps []string, + preferedContents *[]string, +) (*nfpm.Info, error) { + pkgInfo := getBasePkgInfo(vars) + pkgInfo.Description = vars.Description + pkgInfo.Platform = "linux" + pkgInfo.Homepage = vars.Homepage + pkgInfo.License = strings.Join(vars.Licenses, ", ") + pkgInfo.Maintainer = vars.Maintainer + pkgInfo.Overridables = nfpm.Overridables{ + Conflicts: vars.Conflicts, + Replaces: vars.Replaces, + Provides: vars.Provides, + Depends: deps, + } + + if pkgFormat == "apk" { + // Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы + pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool { + return s == pkgInfo.Name + }) + } + + pkgInfo.Release = overrides.ReleasePlatformSpecific(vars.Release, info) + + if vars.Epoch != 0 { + pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10) + } + + setScripts(vars, pkgInfo, dirs.ScriptDir) + + if slices.Contains(vars.Architectures, "all") { + pkgInfo.Arch = "all" + } + + contents, err := buildContents(vars, dirs, preferedContents) + if err != nil { + return nil, err + } + pkgInfo.Overridables.Contents = contents + + if len(vars.AutoProv) == 1 && decoder.IsTruthy(vars.AutoProv[0]) { + if pkgFormat == "rpm" { + err = rpmFindProvides(ctx, pkgInfo, dirs) + if err != nil { + return nil, err + } + } else { + slog.Info(gotext.Get("AutoProv is not implemented for this package format, so it's skipped")) + } + } + + if len(vars.AutoReq) == 1 && decoder.IsTruthy(vars.AutoReq[0]) { + if pkgFormat == "rpm" { + err = rpmFindRequires(ctx, pkgInfo, dirs) + if err != nil { + return nil, err + } + } else { + slog.Info(gotext.Get("AutoReq is not implemented for this package format, so it's skipped")) + } + } + + return pkgInfo, nil +} + +// Функция buildContents создает секцию содержимого пакета, которая содержит файлы, +// которые будут включены в конечный пакет. +func buildContents(vars *types.BuildVars, dirs types.Directories, preferedContents *[]string) ([]*files.Content, error) { + contents := []*files.Content{} + + processPath := func(path, trimmed string, prefered bool) error { + fi, err := os.Lstat(path) + if err != nil { + return err + } + + if fi.IsDir() { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + if !prefered { + _, err = f.Readdirnames(1) + if err != io.EOF { + return nil + } + } + + contents = append(contents, &files.Content{ + Source: path, + Destination: trimmed, + Type: "dir", + FileInfo: &files.ContentFileInfo{ + MTime: fi.ModTime(), + }, + }) + return nil + } + + if fi.Mode()&os.ModeSymlink != 0 { + link, err := os.Readlink(path) + if err != nil { + return err + } + link = strings.TrimPrefix(link, dirs.PkgDir) + + contents = append(contents, &files.Content{ + Source: link, + Destination: trimmed, + Type: "symlink", + FileInfo: &files.ContentFileInfo{ + MTime: fi.ModTime(), + Mode: fi.Mode(), + }, + }) + return nil + } + + fileContent := &files.Content{ + Source: path, + Destination: trimmed, + FileInfo: &files.ContentFileInfo{ + MTime: fi.ModTime(), + Mode: fi.Mode(), + Size: fi.Size(), + }, + } + + if slices.Contains(vars.Backup, trimmed) { + fileContent.Type = "config|noreplace" + } + + contents = append(contents, fileContent) + return nil + } + + if preferedContents != nil { + for _, trimmed := range *preferedContents { + path := filepath.Join(dirs.PkgDir, trimmed) + if err := processPath(path, trimmed, true); err != nil { + return nil, err + } + } + } else { + err := filepath.Walk(dirs.PkgDir, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + trimmed := strings.TrimPrefix(path, dirs.PkgDir) + return processPath(path, trimmed, false) + }) + if err != nil { + return nil, err + } + } + + return contents, nil +} + +// Функция checkForBuiltPackage пытается обнаружить ранее собранный пакет и вернуть его путь +// и true, если нашла. Если нет, возвратит "", false, nil. +func checkForBuiltPackage(mgr manager.Manager, vars *types.BuildVars, pkgFormat, baseDir string) (string, bool, error) { + filename, err := pkgFileName(vars, pkgFormat) + if err != nil { + return "", false, err + } + + pkgPath := filepath.Join(baseDir, filename) + + _, err = os.Stat(pkgPath) + if err != nil { + return "", false, nil + } + + return pkgPath, true, nil +} + +func getBasePkgInfo(vars *types.BuildVars) *nfpm.Info { + return &nfpm.Info{ + Name: vars.Name, + Arch: cpu.Arch(), + Version: vars.Version, + Release: strconv.Itoa(vars.Release), + Epoch: strconv.FormatUint(uint64(vars.Epoch), 10), + } +} + +// pkgFileName returns the filename of the package if it were to be built. +// This is used to check if the package has already been built. +func pkgFileName(vars *types.BuildVars, pkgFormat string) (string, error) { + pkgInfo := getBasePkgInfo(vars) + + packager, err := nfpm.Get(pkgFormat) + if err != nil { + return "", err + } + + return packager.ConventionalFileName(pkgInfo), nil +} + +// Функция getPkgFormat возвращает формат пакета из менеджера пакетов, +// или ALR_PKG_FORMAT, если он установлен. +func getPkgFormat(mgr manager.Manager) string { + pkgFormat := mgr.Format() + if format, ok := os.LookupEnv("ALR_PKG_FORMAT"); ok { + pkgFormat = format + } + return pkgFormat +} + +// Функция createBuildEnvVars создает переменные окружения, которые будут установлены +// в скрипте сборки при его выполнении. +func createBuildEnvVars(info *distro.OSRelease, dirs types.Directories) []string { + env := os.Environ() + + env = append( + env, + "DISTRO_NAME="+info.Name, + "DISTRO_PRETTY_NAME="+info.PrettyName, + "DISTRO_ID="+info.ID, + "DISTRO_VERSION_ID="+info.VersionID, + "DISTRO_ID_LIKE="+strings.Join(info.Like, " "), + "ARCH="+cpu.Arch(), + "NCPU="+strconv.Itoa(runtime.NumCPU()), + ) + + if dirs.ScriptDir != "" { + env = append(env, "scriptdir="+dirs.ScriptDir) + } + + if dirs.PkgDir != "" { + env = append(env, "pkgdir="+dirs.PkgDir) + } + + if dirs.SrcDir != "" { + env = append(env, "srcdir="+dirs.SrcDir) + } + + return env +} + +// Функция setScripts добавляет скрипты-перехватчики к метаданным пакета. +func setScripts(vars *types.BuildVars, info *nfpm.Info, scriptDir string) { + if vars.Scripts.PreInstall != "" { + info.Scripts.PreInstall = filepath.Join(scriptDir, vars.Scripts.PreInstall) + } + + if vars.Scripts.PostInstall != "" { + info.Scripts.PostInstall = filepath.Join(scriptDir, vars.Scripts.PostInstall) + } + + if vars.Scripts.PreRemove != "" { + info.Scripts.PreRemove = filepath.Join(scriptDir, vars.Scripts.PreRemove) + } + + if vars.Scripts.PostRemove != "" { + info.Scripts.PostRemove = filepath.Join(scriptDir, vars.Scripts.PostRemove) + } + + if vars.Scripts.PreUpgrade != "" { + info.ArchLinux.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade) + info.APK.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade) + } + + if vars.Scripts.PostUpgrade != "" { + info.ArchLinux.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade) + info.APK.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade) + } + + if vars.Scripts.PreTrans != "" { + info.RPM.Scripts.PreTrans = filepath.Join(scriptDir, vars.Scripts.PreTrans) + } + + if vars.Scripts.PostTrans != "" { + info.RPM.Scripts.PostTrans = filepath.Join(scriptDir, vars.Scripts.PostTrans) + } +} + +// Функция setVersion изменяет переменную версии в скрипте runner. +// Она используется для установки версии на вывод функции version(). +func setVersion(ctx context.Context, r *interp.Runner, to string) error { + fl, err := syntax.NewParser().Parse(strings.NewReader("version='"+to+"'"), "") + if err != nil { + return err + } + return r.Run(ctx, fl) +} + +// Returns not installed dependencies +func removeAlreadyInstalled(opts types.BuildOpts, dependencies []string) ([]string, error) { + filteredPackages := []string{} + + for _, dep := range dependencies { + installed, err := opts.Manager.IsInstalled(dep) + if err != nil { + return nil, err + } + if installed { + continue + } + filteredPackages = append(filteredPackages, dep) + } + + return filteredPackages, nil +} + +// Функция packageNames возвращает имена всех предоставленных пакетов. +func packageNames(pkgs []db.Package) []string { + names := make([]string, len(pkgs)) + for i, p := range pkgs { + names[i] = p.Name + } + return names +} + +// Функция removeDuplicates убирает любые дубликаты из предоставленного среза. +func removeDuplicates(slice []string) []string { + seen := map[string]struct{}{} + result := []string{} + + for _, s := range slice { + if _, ok := seen[s]; !ok { + seen[s] = struct{}{} + result = append(result, s) + } + } + + return result +} diff --git a/pkg/build/install.go b/pkg/build/install.go index 902fb93..bdb8c2e 100644 --- a/pkg/build/install.go +++ b/pkg/build/install.go @@ -30,6 +30,8 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" "gitea.plemya-x.ru/Plemya-x/ALR/internal/types" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos" ) // InstallPkgs устанавливает нативные пакеты с использованием менеджера пакетов, @@ -45,27 +47,40 @@ func InstallPkgs(ctx context.Context, alrPkgs []db.Package, nativePkgs []string, } } - InstallScripts(ctx, GetScriptPaths(ctx, alrPkgs), opts) + InstallALRPackages(ctx, alrPkgs, opts) // Устанавливаем скрипты сборки через функцию InstallScripts } -// GetScriptPaths возвращает срез путей к скриптам, соответствующий -// данным пакетам -func GetScriptPaths(ctx context.Context, pkgs []db.Package) []string { - var scripts []string - for _, pkg := range pkgs { - // Для каждого пакета создаем путь к скрипту сборки - scriptPath := filepath.Join(config.GetPaths(ctx).RepoDir, pkg.Repository, pkg.Name, "alr.sh") - scripts = append(scripts, scriptPath) +func UpdateOpts(ctx context.Context, opts *types.BuildOpts, pkg *db.Package) { + repodir := config.GetPaths(ctx).RepoDir + if pkg.BasePkgName != "" { + opts.Script = filepath.Join(repodir, pkg.Repository, pkg.BasePkgName, "alr.sh") + opts.Package = pkg.Name + } else { + opts.Script = filepath.Join(repodir, pkg.Repository, pkg.Name, "alr.sh") } - return scripts } -// InstallScripts строит и устанавливает переданные alr скрипты сборки -func InstallScripts(ctx context.Context, scripts []string, opts types.BuildOpts) { - for _, script := range scripts { - opts.Script = script // Устанавливаем текущий скрипт в опции - builtPkgs, _, err := BuildPackage(ctx, opts) +// InstallALRPackages строит и устанавливает переданные alr скрипты сборки +func InstallALRPackages(ctx context.Context, pkgs []db.Package, opts types.BuildOpts) { + info, err := distro.ParseOSRelease(ctx) + if err != nil { + slog.Error(gotext.Get("Error parsing os release"), "err", err) + os.Exit(1) + } + + for _, pkg := range pkgs { + UpdateOpts(ctx, &opts, &pkg) + + builder := New( + ctx, + opts, + repos.GetInstance(ctx), + info, + config.GetInstance(ctx), + ) + + builtPkgs, _, err := builder.BuildPackage(ctx) // Выполняем сборку пакета if err != nil { slog.Error(gotext.Get("Error building package"), "err", err) diff --git a/pkg/repos/pull.go b/pkg/repos/pull.go index 1d41a52..bc66824 100644 --- a/pkg/repos/pull.go +++ b/pkg/repos/pull.go @@ -22,6 +22,8 @@ package repos import ( "context" "errors" + "fmt" + "io" "log/slog" "net/url" "os" @@ -41,8 +43,10 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers" "gitea.plemya-x.ru/Plemya-x/ALR/internal/types" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" ) type actionType uint8 @@ -177,6 +181,96 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { return nil } +func (rs *Repos) updatePkg(ctx context.Context, repo types.Repo, runner *interp.Runner, scriptFl io.ReadCloser) error { + parser := syntax.NewParser() + + defer scriptFl.Close() + fl, err := parser.Parse(scriptFl, "alr.sh") + if err != nil { + return err + } + + runner.Reset() + err = runner.Run(ctx, fl) + if err != nil { + return err + } + + type packages struct { + BasePkgName string `sh:"basepkg_name"` + Names []string `sh:"name"` + } + + var pkgs packages + + d := decoder.New(&distro.OSRelease{}, runner) + d.Overrides = false + d.LikeDistros = false + err = d.DecodeVars(&pkgs) + if err != nil { + return err + } + + if len(pkgs.Names) > 1 { + if pkgs.BasePkgName == "" { + pkgs.BasePkgName = pkgs.Names[0] + } + for _, pkgName := range pkgs.Names { + pkgInfo := PackageInfo{} + funcName := fmt.Sprintf("meta_%s", pkgName) + runner.Reset() + err = runner.Run(ctx, fl) + if err != nil { + return err + } + meta, ok := d.GetFuncSub(funcName) + if !ok { + return errors.New("func is missing") + } + r, err := meta(ctx) + if err != nil { + return err + } + d := decoder.New(&distro.OSRelease{}, r) + d.Overrides = false + d.LikeDistros = false + err = d.DecodeVars(&pkgInfo) + if err != nil { + return err + } + pkg := pkgInfo.ToPackage(repo.Name) + resolveOverrides(r, pkg) + pkg.Name = pkgName + pkg.BasePkgName = pkgs.BasePkgName + err = rs.db.InsertPackage(ctx, *pkg) + if err != nil { + return err + } + } + return nil + } + + pkg := EmptyPackage(repo.Name) + err = d.DecodeVars(pkg) + if err != nil { + return err + } + resolveOverrides(runner, pkg) + return rs.db.InsertPackage(ctx, *pkg) +} + +func (rs *Repos) processRepoChangesRunner(repoDir, scriptDir string) (*interp.Runner, error) { + env := append(os.Environ(), "scriptdir="+scriptDir) + return interp.New( + interp.Env(expand.ListEnviron(env...)), + interp.ExecHandler(handlers.NopExec), + interp.ReadDirHandler(handlers.RestrictedReadDir(repoDir)), + interp.StatHandler(handlers.RestrictedStat(repoDir)), + interp.OpenHandler(handlers.RestrictedOpen(repoDir)), + interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}), + ) +} + func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git.Repository, w *git.Worktree, old, new *plumbing.Reference) error { oldCommit, err := r.CommitObject(old.Hash()) if err != nil { @@ -235,15 +329,7 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git parser := syntax.NewParser() for _, action := range actions { - env := append(os.Environ(), "scriptdir="+filepath.Dir(filepath.Join(repoDir, action.File))) - runner, err := interp.New( - interp.Env(expand.ListEnviron(env...)), - interp.ExecHandler(handlers.NopExec), - interp.ReadDirHandler(handlers.RestrictedReadDir(repoDir)), - interp.StatHandler(handlers.RestrictedStat(repoDir)), - interp.OpenHandler(handlers.RestrictedOpen(repoDir)), - interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}), - ) + runner, err := rs.processRepoChangesRunner(repoDir, filepath.Dir(filepath.Join(repoDir, action.File))) if err != nil { return err } @@ -289,23 +375,7 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git return nil } - pkg := db.Package{ - Description: db.NewJSON(map[string]string{}), - Homepage: db.NewJSON(map[string]string{}), - Maintainer: db.NewJSON(map[string]string{}), - Depends: db.NewJSON(map[string][]string{}), - BuildDepends: db.NewJSON(map[string][]string{}), - Repository: repo.Name, - } - - err = parseScript(ctx, parser, runner, r, &pkg) - if err != nil { - return err - } - - resolveOverrides(runner, &pkg) - - err = rs.db.InsertPackage(ctx, pkg) + err = rs.updatePkg(ctx, repo, runner, r) if err != nil { return err } @@ -322,18 +392,8 @@ func (rs *Repos) processRepoFull(ctx context.Context, repo types.Repo, repoDir s return err } - parser := syntax.NewParser() - for _, match := range matches { - env := append(os.Environ(), "scriptdir="+filepath.Dir(match)) - runner, err := interp.New( - interp.Env(expand.ListEnviron(env...)), - interp.ExecHandler(handlers.NopExec), - interp.ReadDirHandler(handlers.RestrictedReadDir(repoDir)), - interp.StatHandler(handlers.RestrictedStat(repoDir)), - interp.OpenHandler(handlers.RestrictedOpen(repoDir)), - interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}), - ) + runner, err := rs.processRepoChangesRunner(repoDir, filepath.Dir(match)) if err != nil { return err } @@ -343,23 +403,7 @@ func (rs *Repos) processRepoFull(ctx context.Context, repo types.Repo, repoDir s return err } - pkg := db.Package{ - Description: db.NewJSON(map[string]string{}), - Homepage: db.NewJSON(map[string]string{}), - Maintainer: db.NewJSON(map[string]string{}), - Depends: db.NewJSON(map[string][]string{}), - BuildDepends: db.NewJSON(map[string][]string{}), - Repository: repo.Name, - } - - err = parseScript(ctx, parser, runner, scriptFl, &pkg) - if err != nil { - return err - } - - resolveOverrides(runner, &pkg) - - err = rs.db.InsertPackage(ctx, pkg) + err = rs.updatePkg(ctx, repo, runner, scriptFl) if err != nil { return err } diff --git a/pkg/repos/pull_internal_test.go b/pkg/repos/pull_internal_test.go new file mode 100644 index 0000000..b209e9c --- /dev/null +++ b/pkg/repos/pull_internal_test.go @@ -0,0 +1,173 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 Евгений Храмов +// +// 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 . + +package repos + +import ( + "context" + "io" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/types" +) + +type TestALRConfig struct{} + +func (c *TestALRConfig) GetPaths(ctx context.Context) *config.Paths { + return &config.Paths{ + DBPath: ":memory:", + } +} + +func (c *TestALRConfig) Repos(ctx context.Context) []types.Repo { + return []types.Repo{ + { + Name: "test", + URL: "https://test", + }, + } +} + +func createReadCloserFromString(input string) io.ReadCloser { + reader := strings.NewReader(input) + return struct { + io.Reader + io.Closer + }{ + Reader: reader, + Closer: io.NopCloser(reader), + } +} + +func TestUpdatePkg(t *testing.T) { + type testCase struct { + name string + file string + verify func(context.Context, *db.Database) + } + + repo := types.Repo{ + Name: "test", + URL: "https://test", + } + + for _, tc := range []testCase{ + { + name: "single package", + file: `name=foo +version='0.0.1' +release=1 +desc="main desc" +deps=('sudo') +build_deps=('golang') +`, + verify: func(ctx context.Context, database *db.Database) { + result, err := database.GetPkgs(ctx, "1 = 1") + assert.NoError(t, err) + pkgCount := 0 + for result.Next() { + var dbPkg db.Package + err = result.StructScan(&dbPkg) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + assert.Equal(t, "foo", dbPkg.Name) + assert.Equal(t, db.NewJSON(map[string]string{"": "main desc"}), dbPkg.Description) + assert.Equal(t, db.NewJSON(map[string][]string{"": {"sudo"}}), dbPkg.Depends) + pkgCount++ + } + assert.Equal(t, 1, pkgCount) + }, + }, + { + name: "multiple package", + file: `basepkg_name=foo +name=( + bar + buz +) +version='0.0.1' +release=1 +desc="main desc" +deps=('sudo') +build_deps=('golang') + +meta_bar() { + desc="foo desc" +} + +meta_buz() { + deps+=('doas') +} +`, + verify: func(ctx context.Context, database *db.Database) { + result, err := database.GetPkgs(ctx, "1 = 1") + assert.NoError(t, err) + + pkgCount := 0 + for result.Next() { + var dbPkg db.Package + err = result.StructScan(&dbPkg) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + if dbPkg.Name == "bar" { + assert.Equal(t, db.NewJSON(map[string]string{"": "foo desc"}), dbPkg.Description) + assert.Equal(t, db.NewJSON(map[string][]string{"": {"sudo"}}), dbPkg.Depends) + } + + if dbPkg.Name == "buz" { + assert.Equal(t, db.NewJSON(map[string]string{"": "main desc"}), dbPkg.Description) + assert.Equal(t, db.NewJSON(map[string][]string{"": {"sudo", "doas"}}), dbPkg.Depends) + } + pkgCount++ + } + assert.Equal(t, 2, pkgCount) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + cfg := &TestALRConfig{} + ctx := context.Background() + + database := db.New(&TestALRConfig{}) + database.Init(ctx) + + rs := New(cfg, database) + + path, err := os.MkdirTemp("", "test-update-pkg") + assert.NoError(t, err) + defer os.RemoveAll(path) + + runner, err := rs.processRepoChangesRunner(path, path) + assert.NoError(t, err) + + err = rs.updatePkg(ctx, repo, runner, createReadCloserFromString( + tc.file, + )) + assert.NoError(t, err) + + tc.verify(ctx, database) + }) + } +} diff --git a/pkg/repos/repos_legacy.go b/pkg/repos/repos_legacy.go index bdf66ef..046b6fb 100644 --- a/pkg/repos/repos_legacy.go +++ b/pkg/repos/repos_legacy.go @@ -21,7 +21,6 @@ import ( "sync" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" - "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" "gitea.plemya-x.ru/Plemya-x/ALR/internal/types" ) @@ -41,7 +40,7 @@ func Pull(ctx context.Context, repos []types.Repo) error { // It also returns a slice that contains the names of all packages that were not found. // // Deprecated: use struct method -func FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) { +func FindPkgs(ctx context.Context, pkgs []string) (map[string][]database.Package, []string, error) { return GetInstance(ctx).FindPkgs(ctx, pkgs) } diff --git a/pkg/repos/utils.go b/pkg/repos/utils.go index fc6b615..58b1ed3 100644 --- a/pkg/repos/utils.go +++ b/pkg/repos/utils.go @@ -67,6 +67,47 @@ func parseScript(ctx context.Context, parser *syntax.Parser, runner *interp.Runn return d.DecodeVars(pkg) } +type PackageInfo struct { + Version string `sh:"version,required"` + Release int `sh:"release,required"` + Epoch uint `sh:"epoch"` + Architectures db.JSON[[]string] `sh:"architectures"` + Licenses db.JSON[[]string] `sh:"license"` + Provides db.JSON[[]string] `sh:"provides"` + Conflicts db.JSON[[]string] `sh:"conflicts"` + Replaces db.JSON[[]string] `sh:"replaces"` +} + +func (inf *PackageInfo) ToPackage(repoName string) *db.Package { + return &db.Package{ + Version: inf.Version, + Release: inf.Release, + Epoch: inf.Epoch, + Architectures: inf.Architectures, + Licenses: inf.Licenses, + Provides: inf.Provides, + Conflicts: inf.Conflicts, + Replaces: inf.Replaces, + Description: db.NewJSON(map[string]string{}), + Homepage: db.NewJSON(map[string]string{}), + Maintainer: db.NewJSON(map[string]string{}), + Depends: db.NewJSON(map[string][]string{}), + BuildDepends: db.NewJSON(map[string][]string{}), + Repository: repoName, + } +} + +func EmptyPackage(repoName string) *db.Package { + return &db.Package{ + Description: db.NewJSON(map[string]string{}), + Homepage: db.NewJSON(map[string]string{}), + Maintainer: db.NewJSON(map[string]string{}), + Depends: db.NewJSON(map[string][]string{}), + BuildDepends: db.NewJSON(map[string][]string{}), + Repository: repoName, + } +} + var overridable = map[string]string{ "deps": "Depends", "build_deps": "BuildDepends",